// ... UpdateRouter will have CancelSessionHandler and cancel_session route instead of close_recruitment using GmRelay.Bot.Features.Confirmation.HandleRsvp; using GmRelay.Bot.Features.Sessions.CreateSession; using GmRelay.Bot.Features.Sessions.ListSessions; using GmRelay.Bot.Features.Sessions.ExportCalendar; using GmRelay.Bot.Features.Sessions.RescheduleSession; using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; namespace GmRelay.Bot.Infrastructure.Telegram; /// /// Routes incoming Telegram updates to the appropriate feature handler. /// No reflection — all routing is explicit (AOT-safe). /// public sealed class UpdateRouter( HandleRsvpHandler rsvpHandler, CreateSessionHandler createSessionHandler, JoinSessionHandler joinSessionHandler, CancelSessionHandler cancelSessionHandler, DeleteSessionHandler deleteSessionHandler, ListSessionsHandler listSessionsHandler, ExportCalendarHandler exportCalendarHandler, InitiateRescheduleHandler initiateRescheduleHandler, HandleRescheduleTimeInputHandler rescheduleTimeInputHandler, HandleRescheduleVoteHandler rescheduleVoteHandler, ITelegramBotClient bot, ILogger logger) { public async Task RouteAsync(Update update, CancellationToken ct) { switch (update) { case { CallbackQuery: { } query }: await HandleCallbackQueryAsync(query, ct); break; case { Message: { Text: { } text } message } when text.StartsWith('/'): await HandleCommandAsync(message, text, ct); break; // Non-command text messages — check for reschedule time input case { Message: { Text: { } } message } when !message.Text!.StartsWith('/'): await rescheduleTimeInputHandler.TryHandleAsync(message, ct); break; } } private async Task HandleCallbackQueryAsync(CallbackQuery query, CancellationToken ct) { if (query.Data is not { } data || query.Message is not { } message) return; var parts = data.Split(':', 3); var action = parts[0]; if (action == "join_session" && parts.Length >= 2 && Guid.TryParse(parts[1], out var joinSessionId)) { var command = new JoinSessionCommand( SessionId: joinSessionId, TelegramUserId: query.From.Id, DisplayName: query.From.FirstName + (string.IsNullOrEmpty(query.From.LastName) ? "" : $" {query.From.LastName}"), TelegramUsername: query.From.Username, CallbackQueryId: query.Id, ChatId: message.Chat.Id, MessageId: message.MessageId); await joinSessionHandler.HandleAsync(command, ct); return; } if (action == "cancel_session" && parts.Length >= 2 && Guid.TryParse(parts[1], out var cancelSessionId)) { var command = new CancelSessionCommand( SessionId: cancelSessionId, TelegramUserId: query.From.Id, CallbackQueryId: query.Id, ChatId: message.Chat.Id, MessageId: message.MessageId); await cancelSessionHandler.HandleAsync(command, ct); return; } if (action == "delete_session" && parts.Length >= 2 && Guid.TryParse(parts[1], out var deleteSessionId)) { var command = new DeleteSessionCommand( SessionId: deleteSessionId, TelegramUserId: query.From.Id, CallbackQueryId: query.Id, ChatId: message.Chat.Id, MessageId: message.MessageId); await deleteSessionHandler.HandleAsync(command, ct); return; } if (action == "reschedule_session" && parts.Length >= 2 && Guid.TryParse(parts[1], out var rescheduleSessionId)) { var command = new InitiateRescheduleCommand( SessionId: rescheduleSessionId, TelegramUserId: query.From.Id, CallbackQueryId: query.Id, ChatId: message.Chat.Id, MessageId: message.MessageId); await initiateRescheduleHandler.HandleAsync(command, ct); return; } if (action == "reschedule_vote" && parts.Length >= 3 && Guid.TryParse(parts[2], out var proposalId)) { var vote = parts[1]; // "yes" or "no" if (vote is not ("yes" or "no")) return; var command = new HandleRescheduleVoteCommand( ProposalId: proposalId, Vote: vote, TelegramUserId: query.From.Id, CallbackQueryId: query.Id, ChatId: message.Chat.Id, MessageId: message.MessageId); await rescheduleVoteHandler.HandleAsync(command, ct); return; } if (action == "rsvp") { if (parts.Length < 3 || !Guid.TryParse(parts[2], out var sessionId)) return; var status = parts[1] switch { "confirm" => Domain.RsvpStatus.Confirmed, "decline" => Domain.RsvpStatus.Declined, _ => (string?)null }; if (status is null) return; var command = new HandleRsvpCommand( SessionId: sessionId, TelegramUserId: query.From.Id, Status: status, CallbackQueryId: query.Id, ChatId: message.Chat.Id, MessageId: message.MessageId); await rsvpHandler.HandleAsync(command, ct); } } private async Task HandleCommandAsync(Message message, string text, CancellationToken ct) { // Извлекаем команду: берём первую строку, первое слово, убираем @BotUsername var firstLine = text.Split('\n')[0].Trim(); var command = firstLine.Split(' ')[0].Split('@')[0].ToLowerInvariant(); switch (command) { case "/start": await bot.SendMessage( chatId: message.Chat.Id, text: "GM-Relay Bot ready. Use /help for commands.", cancellationToken: ct); break; case "/newsession": await createSessionHandler.HandleAsync(message, ct); break; case "/listsessions": await listSessionsHandler.HandleAsync(message, ct); break; case "/exportcalendar": await exportCalendarHandler.HandleAsync(message, ct); break; case "/help": await bot.SendMessage( chatId: message.Chat.Id, text: """ GM-Relay — бот для управления игровыми сессиями. /newsession Название: My Game Время: 15.05.2026 19:30 Ссылка: https://link /listsessions — список предстоящих сессий /help — эта справка """, cancellationToken: ct); break; // TODO: /listsessions — will be implemented as features default: logger.LogDebug("Unknown command: {Command}", command); break; } } }