259 lines
9.9 KiB
C#
259 lines
9.9 KiB
C#
// ... UpdateRouter will have CancelSessionHandler and cancel_session route instead of close_recruitment
|
||
using GmRelay.Shared.Domain;
|
||
using GmRelay.Shared.Rendering;
|
||
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;
|
||
using Telegram.Bot.Types.ReplyMarkups;
|
||
|
||
namespace GmRelay.Bot.Infrastructure.Telegram;
|
||
|
||
/// <summary>
|
||
/// Routes incoming Telegram updates to the appropriate feature handler.
|
||
/// No reflection — all routing is explicit (AOT-safe).
|
||
/// </summary>
|
||
public sealed class UpdateRouter(
|
||
HandleRsvpHandler rsvpHandler,
|
||
CreateSessionHandler createSessionHandler,
|
||
JoinSessionHandler joinSessionHandler,
|
||
LeaveSessionHandler leaveSessionHandler,
|
||
PromoteWaitlistedPlayerHandler promoteWaitlistedPlayerHandler,
|
||
CancelSessionHandler cancelSessionHandler,
|
||
DeleteSessionHandler deleteSessionHandler,
|
||
ListSessionsHandler listSessionsHandler,
|
||
ExportCalendarHandler exportCalendarHandler,
|
||
InitiateRescheduleHandler initiateRescheduleHandler,
|
||
HandleRescheduleTimeInputHandler rescheduleTimeInputHandler,
|
||
HandleRescheduleVoteHandler rescheduleVoteHandler,
|
||
ITelegramBotClient bot,
|
||
IConfiguration configuration,
|
||
ILogger<UpdateRouter> logger) : ITelegramUpdateHandler
|
||
{
|
||
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 == "leave_session" && parts.Length >= 2 && Guid.TryParse(parts[1], out var leaveSessionId))
|
||
{
|
||
var command = new LeaveSessionCommand(
|
||
SessionId: leaveSessionId,
|
||
TelegramUserId: query.From.Id,
|
||
CallbackQueryId: query.Id,
|
||
ChatId: message.Chat.Id,
|
||
MessageId: message.MessageId);
|
||
|
||
await leaveSessionHandler.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 == "promote_waitlist" && parts.Length >= 2 && Guid.TryParse(parts[1], out var promoteSessionId))
|
||
{
|
||
var command = new PromoteWaitlistedPlayerCommand(
|
||
SessionId: promoteSessionId,
|
||
TelegramUserId: query.From.Id,
|
||
CallbackQueryId: query.Id,
|
||
ChatId: message.Chat.Id,
|
||
MessageId: message.MessageId);
|
||
|
||
await promoteWaitlistedPlayerHandler.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 >= 2 && Guid.TryParse(parts[1], out var optionId))
|
||
{
|
||
var command = new HandleRescheduleVoteCommand(
|
||
OptionId: optionId,
|
||
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" => RsvpStatus.Confirmed,
|
||
"decline" => 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 SendStartMessageAsync(message, 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
|
||
Мест: 4
|
||
Ссылка: https://link
|
||
|
||
Для регулярного расписания можно указать одну дату:
|
||
Игр: 4
|
||
Интервал: 7
|
||
|
||
/listsessions — список предстоящих сессий
|
||
Игроки могут записаться кнопкой «На дату» и сняться кнопкой «Выйти».
|
||
Owner и co-GM могут переносить сессии кнопкой «Перенести»: бот попросит 2-3 варианта времени и дедлайн голосования.
|
||
/help — эта справка
|
||
""",
|
||
cancellationToken: ct);
|
||
break;
|
||
|
||
// TODO: /listsessions — will be implemented as features
|
||
default:
|
||
logger.LogDebug("Unknown command: {Command}", command);
|
||
break;
|
||
}
|
||
}
|
||
|
||
private async Task SendStartMessageAsync(Message message, CancellationToken ct)
|
||
{
|
||
var miniAppUrl = configuration["Telegram:MiniAppUrl"];
|
||
if (string.IsNullOrWhiteSpace(miniAppUrl))
|
||
{
|
||
await bot.SendMessage(
|
||
chatId: message.Chat.Id,
|
||
text: "GM-Relay Bot ready. Use /help for commands.",
|
||
cancellationToken: ct);
|
||
return;
|
||
}
|
||
|
||
await bot.SendMessage(
|
||
chatId: message.Chat.Id,
|
||
text: "GM-Relay Bot ready. Откройте dashboard внутри Telegram или используйте /help для команд.",
|
||
replyMarkup: new InlineKeyboardMarkup(
|
||
InlineKeyboardButton.WithWebApp("Открыть dashboard", new WebAppInfo(miniAppUrl))),
|
||
cancellationToken: ct);
|
||
}
|
||
}
|