e791fc2f4a
PR Checks / test-and-build (pull_request) Successful in 5m3s
Convert join/leave interaction commands to PlatformUser, PlatformGroup, and PlatformMessageRef. Persist and look up participants by platform identity while keeping Telegram callbacks intact. Add V017 migration and TDD coverage. Bump version to 2.1.1.
275 lines
10 KiB
C#
275 lines
10 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: { } message }:
|
||
var commandText = GetCommandText(message);
|
||
if (commandText.StartsWith("/", StringComparison.Ordinal))
|
||
{
|
||
await HandleCommandAsync(message, commandText, ct);
|
||
break;
|
||
}
|
||
|
||
if (message.Text is not null)
|
||
{
|
||
await rescheduleTimeInputHandler.TryHandleAsync(message, ct);
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
internal static string GetCommandText(Message message)
|
||
=> (message.Text ?? message.Caption ?? string.Empty).TrimStart();
|
||
|
||
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];
|
||
var user = TelegramPlatformIds.User(
|
||
query.From.Id,
|
||
query.From.FirstName + (string.IsNullOrEmpty(query.From.LastName) ? "" : $" {query.From.LastName}"),
|
||
query.From.Username);
|
||
var group = TelegramPlatformIds.Group(message.Chat.Id, message.MessageThreadId, message.Chat.Title);
|
||
var scheduleMessage = TelegramPlatformIds.Message(message.Chat.Id, message.MessageThreadId, message.MessageId);
|
||
|
||
if (action == "join_session" && parts.Length >= 2 && Guid.TryParse(parts[1], out var joinSessionId))
|
||
{
|
||
var command = new JoinSessionCommand(
|
||
SessionId: joinSessionId,
|
||
User: user,
|
||
InteractionId: query.Id,
|
||
Group: group,
|
||
ScheduleMessage: scheduleMessage);
|
||
|
||
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,
|
||
User: user,
|
||
InteractionId: query.Id,
|
||
Group: group,
|
||
ScheduleMessage: scheduleMessage);
|
||
|
||
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,
|
||
MessageThreadId: message.MessageThreadId,
|
||
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,
|
||
MessageThreadId: message.MessageThreadId,
|
||
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
|
||
Картинка: https://cover
|
||
|
||
Для регулярного расписания можно указать одну дату:
|
||
Игр: 4
|
||
Интервал: 7
|
||
|
||
/listsessions — список предстоящих сессий
|
||
Для owner/co-GM /listsessions показывает кнопки отмены, переноса, удаления и повышения из листа ожидания.
|
||
Игроки могут записаться кнопкой «На дату» и сняться кнопкой «Выйти».
|
||
/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);
|
||
}
|
||
}
|