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; /// /// Telegram-side implementation of . /// Translates the platform-neutral wizard contracts into the /// Telegram.Bot SDK calls. All Telegram-specific behaviour /// (message editing, callback ack, group lookup) lives behind the /// interface so the wizard core stays in GmRelay.Shared. /// public sealed class TelegramWizardMessenger( ITelegramBotClient bot, NpgsqlDataSource dataSource) : IWizardMessenger { public async Task EditDraftMessageAsync( WizardDraft draft, string text, IReadOnlyList 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 SendDraftMessageAsync( WizardDraft draft, string text, IReadOnlyList 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> 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. 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( new CommandDefinition( sql, new { Platform = "Telegram", ExternalId = ownerId }, cancellationToken: ct)); 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; } }