Files
GmRelayBot/src/GmRelay.DiscordBot/Program.cs
T
Toutsu 2c9016a383
Deploy Telegram Bot / build-and-push (push) Successful in 7m18s
Deploy Telegram Bot / scan-images (push) Failing after 17s
Deploy Telegram Bot / deploy (push) Has been skipped
fix(shared,bot,discordbot): make club-picker Dapper calls AOT-safe (v3.9.2)
The 3.9.1 hotfix only repaired WizardDraftRepository, the most common
Dapper call in the wizard. The same AOT-unsafe CommandDefinition pattern
remained in 4 other places that the user hit immediately after the
deploy: the 'Choose visibility' wizard step triggers GetOwnerClubsAsync
when the user picks 'Публичная в витрине клуба' or 'Только для членов
клуба'. The wizard swallowed PlatformNotSupportedException, the
callback ack replied with '⚠️ Ошибка', and the next step never rendered.
Privacy 'didn't stick' from the user's perspective.

Two changes to fix the Discord side as well:

1. Switched GetOwnerClubsAsync / LoadClubsAsync / LoadManagerUserIdsAsync
   to the direct (sql, params) overload across TelegramWizardMessenger,
   DiscordWizardMessenger, DiscordWizardInteractionModule, and
   DiscordPermissionLookup — same pattern as the 3.9.1 fix.

2. Added Dapper.AOT module attribute ([module: Dapper.DapperAot]) and
   InterceptorsPreviewNamespaces to the DiscordBot project. The
   DiscordBot assembly was previously skipped by the AOT source
   generator, so even the direct-overload fix wouldn't have produced
   interceptors for the Discord-specific Dapper call sites. With this
   addition, the generator emits 3 DiscordBot-specific interceptors
   (DiscordWizardMessenger, DiscordWizardInteractionModule,
   DiscordPermissionLookup) and the AssemblyLoad ships with the right
   GmRelay.DiscordBot.generated.cs.

Also expanded the AOT shape regression tests to cover all 4
CommandDefinition sites + added a 'containingClass' parameter to
ExtractMethodBody to disambiguate the duplicated LoadClubsAsync names
in DiscordWizardInteractionModule.

Bumps: 3.9.1 -> 3.9.2.
2026-06-08 10:48:24 +03:00

124 lines
6.1 KiB
C#

using GmRelay.DiscordBot;
using GmRelay.DiscordBot.Features.Sessions;
using GmRelay.DiscordBot.Features.Sessions.Wizard;
using GmRelay.DiscordBot.Infrastructure;
using GmRelay.DiscordBot.Infrastructure.Discord;
using GmRelay.DiscordBot.Infrastructure.Health;
using GmRelay.DiscordBot.Infrastructure.Logging;
using GmRelay.Shared.Features.Confirmation.HandleRsvp;
using GmRelay.Shared.Features.Confirmation.SendConfirmation;
using GmRelay.Shared.Features.Notifications;
using GmRelay.Shared.Features.Reminders.SendJoinLink;
using GmRelay.Shared.Features.Reminders.SendOneHourReminder;
using GmRelay.Shared.Features.Sessions.CreateSession;
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
using GmRelay.Shared.Features.Sessions.RescheduleSession;
using GmRelay.Shared.Infrastructure.Scheduling;
using GmRelay.Shared.Platform;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NetCord;
using NetCord.Gateway;
using NetCord.Hosting.Gateway;
using NetCord.Hosting.Services;
using NetCord.Hosting.Services.ApplicationCommands;
using NetCord.Hosting.Services.ComponentInteractions;
using NetCord.Services.ApplicationCommands;
using NetCord.Services.ComponentInteractions;
using Npgsql;
[module: Dapper.DapperAot]
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
var discordOptions = builder.Configuration
.GetRequiredSection("Discord")
.Get<DiscordOptions>() ?? new DiscordOptions();
discordOptions.Validate();
builder.Services.AddSingleton(discordOptions);
builder.Logging.AddConsole();
builder.Services.AddSingleton<NpgsqlDataSource>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var connectionString = config.GetConnectionString("gmrelaydb")
?? throw new InvalidOperationException(
"ConnectionStrings:gmrelaydb is required. Set via environment variable ConnectionStrings__gmrelaydb.");
var logger = loggerFactory.CreateLogger("GmRelay.DiscordBot.Startup");
logger.LogInformation(
"Configured PostgreSQL data source with connection string {ConnectionString}",
SecretRedactor.RedactConnectionString(connectionString));
return NpgsqlDataSource.Create(connectionString);
});
builder.Services.AddSingleton<DiscordPermissionChecker>();
builder.Services.AddSingleton<DiscordListSessionsHandler>();
builder.Services.AddSingleton<DiscordDeleteSessionHandler>();
builder.Services.AddSingleton<DiscordNewSessionHandler>();
builder.Services.AddSingleton<DiscordRescheduleHandler>();
builder.Services.AddSingleton<GmRelay.Shared.Features.Sessions.RescheduleSession.HandleRescheduleVoteHandler>();
builder.Services.AddSingleton<DiscordRescheduleVoteHandler>();
builder.Services.AddSingleton<IScheduleMessageUpdateLock, ScheduleMessageUpdateLock>();
builder.Services.AddSingleton<JoinSessionHandler>();
builder.Services.AddSingleton<LeaveSessionHandler>();
builder.Services.AddSingleton<DiscordInteractionReplyCache>();
builder.Services.AddSingleton<IPlatformMessenger, DiscordPlatformMessenger>();
builder.Services.AddSingleton<ISystemClock, SystemClock>();
builder.Services.AddSingleton(new PlatformSchedulerOptions(PlatformKind.Discord));
builder.Services.AddSingleton<ISessionTriggerStore, DbSessionTriggerStore>();
builder.Services.AddSingleton<PlatformDirectNotificationSender>();
builder.Services.AddSingleton<SendConfirmationHandler>();
builder.Services.AddSingleton<ISendConfirmationHandler>(sp => sp.GetRequiredService<SendConfirmationHandler>());
builder.Services.AddSingleton<SendJoinLinkHandler>();
builder.Services.AddSingleton<ISendJoinLinkHandler>(sp => sp.GetRequiredService<SendJoinLinkHandler>());
builder.Services.AddSingleton<SendOneHourReminderHandler>();
builder.Services.AddSingleton<ISendOneHourReminderHandler>(sp => sp.GetRequiredService<SendOneHourReminderHandler>());
builder.Services.AddSingleton<HandleRsvpHandler>();
builder.Services.AddSingleton<RescheduleVotingFinalizer>();
builder.Services.AddHostedService<SessionSchedulerService>();
builder.Services.AddHostedService<DiscordRescheduleVotingDeadlineService>();
builder.Services.AddHostedService<DiscordHealthCheckHostedService>();
// ── Wizard services (issue #112) ──────────────────────────────────────
// The Discord wizard reuses the platform-neutral state machine in
// GmRelay.Shared (GameCreationWizard, IWizardMessenger,
// IWizardDraftRepository) and only adds a Discord-specific messenger,
// step renderer, slash command, and submitter on top. The wizard's
// cleanup service is shared with the Telegram bot and is not
// registered here — it would compete on the same drafts table.
builder.Services.AddSingleton<IWizardDraftRepository, GmRelay.Shared.Features.Sessions.CreateSession.Wizard.WizardDraftRepository>();
builder.Services.AddSingleton<IWizardContextStore, DiscordWizardContextStore>();
builder.Services.AddSingleton<IWizardMessenger, DiscordWizardMessenger>();
builder.Services.AddSingleton<GmRelay.Shared.Features.Sessions.CreateSession.Wizard.GameCreationWizard>();
builder.Services.AddSingleton<DiscordWizardSubmitter>();
builder.Services.AddSingleton<WizardInteractionDispatcher>();
builder.Services.AddSingleton<DiscordWizardButtonModule>();
builder.Services.AddSingleton<DiscordWizardStringMenuModule>();
builder.Services.AddSingleton<DiscordWizardModalModule>();
builder.Services
.AddDiscordGateway(options =>
{
options.Token = discordOptions.Token;
options.Intents = GatewayIntents.Guilds;
})
.AddApplicationCommands<SlashCommandInteraction, SlashCommandContext>()
.AddComponentInteractions<ButtonInteraction, ButtonInteractionContext>()
.AddComponentInteractions<StringMenuInteraction, StringMenuInteractionContext>()
.AddComponentInteractions<ModalInteraction, ModalInteractionContext>()
.AddGatewayHandlers(typeof(Program).Assembly);
var host = builder.Build();
host.AddSlashCommand("ping", "Checks whether GM-Relay Discord is online.", () => "Pong!");
host.AddModules(typeof(Program).Assembly);
await host.RunAsync();