2c9016a383
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.
143 lines
5.5 KiB
C#
143 lines
5.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Dapper;
|
|
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
|
using Npgsql;
|
|
using Telegram.Bot;
|
|
using Telegram.Bot.Types.ReplyMarkups;
|
|
|
|
namespace GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
|
|
|
/// <summary>
|
|
/// Telegram-side implementation of <see cref="IWizardMessenger"/>.
|
|
/// Translates the platform-neutral wizard contracts into the
|
|
/// <c>Telegram.Bot</c> SDK calls. All Telegram-specific behaviour
|
|
/// (message editing, callback ack, group lookup) lives behind the
|
|
/// interface so the wizard core stays in <c>GmRelay.Shared</c>.
|
|
/// </summary>
|
|
public sealed class TelegramWizardMessenger(
|
|
ITelegramBotClient bot,
|
|
NpgsqlDataSource dataSource) : IWizardMessenger
|
|
{
|
|
public async Task<string> EditDraftMessageAsync(
|
|
WizardDraft draft,
|
|
string text,
|
|
IReadOnlyList<WizardAction> keyboard,
|
|
CancellationToken ct)
|
|
{
|
|
if (!TryParseChatId(draft.ChatId, out var chatId))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Wizard draft {draft.Id} has un-parseable chat id '{draft.ChatId}'.");
|
|
}
|
|
if (!TryParseMessageId(draft.DraftMessageId, out var messageId))
|
|
{
|
|
// No draft message recorded yet — fall back to sending a new one.
|
|
return await SendDraftMessageAsync(draft, text, keyboard, ct);
|
|
}
|
|
var msg = await bot.EditMessageText(
|
|
chatId: chatId,
|
|
messageId: messageId,
|
|
text: text,
|
|
replyMarkup: WizardStep.ToInlineKeyboard(keyboard),
|
|
cancellationToken: ct);
|
|
return msg.MessageId.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
public async Task<string> SendDraftMessageAsync(
|
|
WizardDraft draft,
|
|
string text,
|
|
IReadOnlyList<WizardAction> keyboard,
|
|
CancellationToken ct)
|
|
{
|
|
if (!TryParseChatId(draft.ChatId, out var chatId))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Wizard draft {draft.Id} has un-parseable chat id '{draft.ChatId}'.");
|
|
}
|
|
int? threadId = TryParseThreadId(draft.MessageThreadId, out var parsedThread)
|
|
? parsedThread
|
|
: null;
|
|
|
|
var msg = await bot.SendMessage(
|
|
chatId: chatId,
|
|
text: text,
|
|
messageThreadId: threadId,
|
|
replyMarkup: WizardStep.ToInlineKeyboard(keyboard),
|
|
cancellationToken: ct);
|
|
return msg.MessageId.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
public Task AnswerInteractionAsync(string interactionId, string? text, CancellationToken ct)
|
|
{
|
|
return bot.AnswerCallbackQuery(interactionId, text: text, cancellationToken: ct);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<WizardClubOption>> GetOwnerClubsAsync(string ownerId, CancellationToken ct)
|
|
{
|
|
// Adjusted from the plan: this codebase models "clubs" as game_groups
|
|
// (V001 created game_groups; V026 added public_slug; no `clubs` table exists,
|
|
// and game_groups has no `club_id` FK). The picker therefore returns the
|
|
// game_groups the owner manages as a GM (via group_managers), matching
|
|
// the WizardClubOption contract (UUID id, name) used downstream.
|
|
//
|
|
// NativeAOT: Dapper.AOT 1.0.48 only generates interceptors for the
|
|
// (sql, object?) extension overload — not the (CommandDefinition) overload.
|
|
// The wizard reaches this method on the PickClub visibility step
|
|
// (issue #112 follow-up); using CommandDefinition here would fall back
|
|
// to Dapper.SqlMapper.CreateParamInfoGenerator, which uses Reflection.Emit
|
|
// and throws PlatformNotSupportedException on AOT. Same root cause as
|
|
// WizardDraftRepository.GetActiveAsync in v3.9.0, same fix pattern.
|
|
const string sql = """
|
|
SELECT g.id AS ClubId,
|
|
g.name AS Name
|
|
FROM game_groups g
|
|
JOIN group_managers gm ON gm.group_id = g.id
|
|
JOIN players p ON p.id = gm.player_id
|
|
WHERE p.platform = @Platform
|
|
AND p.external_user_id = @ExternalId
|
|
GROUP BY g.id, g.name
|
|
ORDER BY g.name
|
|
""";
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
var rows = await connection.QueryAsync<WizardClubOption>(
|
|
sql,
|
|
new { Platform = "Telegram", ExternalId = ownerId });
|
|
return rows.AsList();
|
|
}
|
|
|
|
private static bool TryParseChatId(string raw, out long chatId)
|
|
{
|
|
if (long.TryParse(raw, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out chatId))
|
|
{
|
|
return true;
|
|
}
|
|
chatId = 0;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryParseMessageId(string? raw, out int messageId)
|
|
{
|
|
if (raw is not null &&
|
|
int.TryParse(raw, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out messageId))
|
|
{
|
|
return true;
|
|
}
|
|
messageId = 0;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryParseThreadId(string? raw, out int threadId)
|
|
{
|
|
if (raw is not null &&
|
|
int.TryParse(raw, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out threadId))
|
|
{
|
|
return true;
|
|
}
|
|
threadId = 0;
|
|
return false;
|
|
}
|
|
}
|