refactor: add platform messenger contracts
PR Checks / test-and-build (pull_request) Successful in 12m35s
PR Checks / test-and-build (pull_request) Successful in 12m35s
Introduce platform-neutral PlatformKind, PlatformUser, PlatformGroup, and IPlatformMessenger contracts in GmRelay.Shared. Route Telegram session schedule updates, direct notifications, interaction replies, and calendar export through TelegramPlatformMessenger while preserving existing Telegram behavior. Bump version -> 2.0.1
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
using GmRelay.Shared.Platform;
|
||||
|
||||
namespace GmRelay.Bot.Features.Notifications;
|
||||
|
||||
public sealed record DirectNotificationRecipient(long TelegramId, string DisplayName);
|
||||
|
||||
public sealed class DirectSessionNotificationSender(
|
||||
ITelegramBotClient bot,
|
||||
IPlatformMessenger messenger,
|
||||
ILogger<DirectSessionNotificationSender> logger)
|
||||
{
|
||||
public async Task SendAsync(
|
||||
@@ -20,11 +20,11 @@ public sealed class DirectSessionNotificationSender(
|
||||
{
|
||||
try
|
||||
{
|
||||
await bot.SendMessage(
|
||||
chatId: recipient.TelegramId,
|
||||
text: htmlText,
|
||||
parseMode: ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
await messenger.SendPrivateMessageAsync(
|
||||
new PlatformPrivateMessage(
|
||||
TelegramPlatformIds.User(recipient.TelegramId, recipient.DisplayName),
|
||||
htmlText),
|
||||
ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Dapper;
|
||||
using GmRelay.Bot.Features.Notifications;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Platform;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
|
||||
namespace GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
@@ -22,7 +21,7 @@ internal sealed record CancelSessionInfoDto(string Title, Guid BatchId, int? Bat
|
||||
|
||||
public sealed class CancelSessionHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient bot,
|
||||
IPlatformMessenger messenger,
|
||||
DirectSessionNotificationSender directSender,
|
||||
ILogger<CancelSessionHandler> logger)
|
||||
{
|
||||
@@ -52,13 +51,13 @@ public sealed class CancelSessionHandler(
|
||||
|
||||
if (session == null)
|
||||
{
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия не найдена.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Сессия не найдена.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.CanManage)
|
||||
{
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Только owner или co-GM может отменять сессию.", showAlert: true, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Только owner или co-GM может отменять сессию.", ct, showAlert: true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,27 +104,24 @@ public sealed class CancelSessionHandler(
|
||||
|
||||
// 4. Перерисовываем сообщение
|
||||
var view = SessionBatchViewBuilder.Build(session.Title, batchSessions.ToList(), batchParticipants.ToList());
|
||||
var renderResult = TelegramSessionBatchRenderer.Render(view);
|
||||
|
||||
try
|
||||
{
|
||||
await BatchMessageEditor.EditBatchMessageAsync(
|
||||
bot,
|
||||
chatId: command.ChatId,
|
||||
messageId: session.BatchMessageId ?? command.MessageId,
|
||||
text: renderResult.Text,
|
||||
replyMarkup: renderResult.Markup,
|
||||
var messageId = session.BatchMessageId ?? command.MessageId;
|
||||
await messenger.UpdateScheduleAsync(
|
||||
new PlatformScheduleMessage(
|
||||
TelegramPlatformIds.Group(command.ChatId, command.MessageThreadId),
|
||||
view,
|
||||
TelegramPlatformIds.Message(command.ChatId, command.MessageThreadId, messageId)),
|
||||
ct);
|
||||
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия отменена!", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Сессия отменена!", ct);
|
||||
|
||||
// Опционально: написать отдельное сообщение в чат
|
||||
await bot.SendMessage(
|
||||
chatId: command.ChatId,
|
||||
messageThreadId: command.MessageThreadId,
|
||||
text: $"❌ <b>Внимание!</b> Сессия \"{System.Net.WebUtility.HtmlEncode(session.Title)}\" отменена.",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
await messenger.SendGroupMessageAsync(
|
||||
TelegramPlatformIds.Group(command.ChatId, command.MessageThreadId),
|
||||
$"❌ <b>Внимание!</b> Сессия \"{System.Net.WebUtility.HtmlEncode(session.Title)}\" отменена.",
|
||||
ct);
|
||||
|
||||
var mode = SessionNotificationModeExtensions.FromDatabaseValue(session.NotificationMode);
|
||||
if (mode.ShouldSendDirectMessages())
|
||||
@@ -141,7 +137,10 @@ public sealed class CancelSessionHandler(
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to update batch message after cancelling session {SessionId}", command.SessionId);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Ошибка при обновлении сообщения.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Ошибка при обновлении сообщения.", ct);
|
||||
}
|
||||
}
|
||||
|
||||
private Task AnswerAsync(string callbackQueryId, string text, CancellationToken ct, bool showAlert = false) =>
|
||||
messenger.AnswerInteractionAsync(new PlatformInteractionReply(callbackQueryId, text, showAlert), ct);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Platform;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
|
||||
@@ -22,7 +21,7 @@ internal sealed record JoinSessionBatchDto(Guid BatchId, string Title, int? MaxP
|
||||
|
||||
public sealed class JoinSessionHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient bot,
|
||||
IPlatformMessenger messenger,
|
||||
ILogger<JoinSessionHandler> logger)
|
||||
{
|
||||
public async Task HandleAsync(JoinSessionCommand command, CancellationToken ct)
|
||||
@@ -59,7 +58,7 @@ public sealed class JoinSessionHandler(
|
||||
if (batchInfo is null)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия не найдена.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Сессия не найдена.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,7 +79,7 @@ public sealed class JoinSessionHandler(
|
||||
var alreadyText = existingRegistrationStatus == ParticipantRegistrationStatus.Waitlisted
|
||||
? "Вы уже в листе ожидания!"
|
||||
: "Вы уже записаны!";
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, alreadyText, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, alreadyText, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,7 +113,7 @@ public sealed class JoinSessionHandler(
|
||||
if (inserted == 0)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Вы уже записаны!", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Вы уже записаны!", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,20 +142,17 @@ public sealed class JoinSessionHandler(
|
||||
|
||||
// 4. Перерисовываем сообщение
|
||||
var view = SessionBatchViewBuilder.Build(batchInfo.Title, batchSessions.ToList(), batchParticipants.ToList());
|
||||
var renderResult = TelegramSessionBatchRenderer.Render(view);
|
||||
|
||||
await BatchMessageEditor.EditBatchMessageAsync(
|
||||
bot,
|
||||
chatId: command.ChatId,
|
||||
messageId: command.MessageId,
|
||||
text: renderResult.Text,
|
||||
replyMarkup: renderResult.Markup,
|
||||
await messenger.UpdateScheduleAsync(
|
||||
new PlatformScheduleMessage(
|
||||
TelegramPlatformIds.Group(command.ChatId),
|
||||
view,
|
||||
TelegramPlatformIds.Message(command.ChatId, threadId: null, command.MessageId)),
|
||||
ct);
|
||||
|
||||
var callbackText = registrationStatus == ParticipantRegistrationStatus.Waitlisted
|
||||
? "Основной состав заполнен. Вы добавлены в лист ожидания."
|
||||
: "Вы успешно записаны!";
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, callbackText, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, callbackText, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -169,7 +165,10 @@ public sealed class JoinSessionHandler(
|
||||
var errorText = transactionCommitted
|
||||
? "Регистрация сохранена, но не удалось обновить сообщение расписания."
|
||||
: "Произошла ошибка при регистрации.";
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, errorText, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, errorText, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private Task AnswerAsync(string callbackQueryId, string text, CancellationToken ct) =>
|
||||
messenger.AnswerInteractionAsync(new PlatformInteractionReply(callbackQueryId, text), ct);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Dapper;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Platform;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
|
||||
namespace GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
@@ -20,7 +20,7 @@ internal sealed record LeaveSessionPromotionDto(Guid ParticipantRowId, string Di
|
||||
|
||||
public sealed class LeaveSessionHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient bot,
|
||||
IPlatformMessenger messenger,
|
||||
ILogger<LeaveSessionHandler> logger)
|
||||
{
|
||||
public async Task HandleAsync(LeaveSessionCommand command, CancellationToken ct)
|
||||
@@ -47,14 +47,14 @@ public sealed class LeaveSessionHandler(
|
||||
if (session is null)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия не найдена.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Сессия не найдена.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SessionStatus.IsCancelled(session.Status))
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия уже отменена.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Сессия уже отменена.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ public sealed class LeaveSessionHandler(
|
||||
if (participant is null)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Вы не записаны на эту сессию.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Вы не записаны на эту сессию.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,14 +185,11 @@ public sealed class LeaveSessionHandler(
|
||||
transactionCommitted = true;
|
||||
|
||||
var view = SessionBatchViewBuilder.Build(session.Title, batchSessions, batchParticipants);
|
||||
var renderResult = TelegramSessionBatchRenderer.Render(view);
|
||||
|
||||
await BatchMessageEditor.EditBatchMessageAsync(
|
||||
bot,
|
||||
chatId: command.ChatId,
|
||||
messageId: command.MessageId,
|
||||
text: renderResult.Text,
|
||||
replyMarkup: renderResult.Markup,
|
||||
await messenger.UpdateScheduleAsync(
|
||||
new PlatformScheduleMessage(
|
||||
TelegramPlatformIds.Group(command.ChatId),
|
||||
view,
|
||||
TelegramPlatformIds.Message(command.ChatId, threadId: null, command.MessageId)),
|
||||
ct);
|
||||
|
||||
var callbackText = participant.RegistrationStatus == ParticipantRegistrationStatus.Waitlisted
|
||||
@@ -201,7 +198,7 @@ public sealed class LeaveSessionHandler(
|
||||
? "Вы отписались от сессии."
|
||||
: $"Вы отписались от сессии. Место получил(а) {promotedDisplayName}.";
|
||||
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, callbackText, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, callbackText, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -214,7 +211,10 @@ public sealed class LeaveSessionHandler(
|
||||
var errorText = transactionCommitted
|
||||
? "Запись снята, но не удалось обновить сообщение расписания."
|
||||
: "Произошла ошибка при отмене записи.";
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, errorText, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, errorText, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private Task AnswerAsync(string callbackQueryId, string text, CancellationToken ct) =>
|
||||
messenger.AnswerInteractionAsync(new PlatformInteractionReply(callbackQueryId, text), ct);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Dapper;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Platform;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
|
||||
namespace GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
@@ -19,7 +19,7 @@ internal sealed record WaitlistedParticipantDto(Guid ParticipantRowId, string Di
|
||||
|
||||
public sealed class PromoteWaitlistedPlayerHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient bot,
|
||||
IPlatformMessenger messenger,
|
||||
ILogger<PromoteWaitlistedPlayerHandler> logger)
|
||||
{
|
||||
public async Task HandleAsync(PromoteWaitlistedPlayerCommand command, CancellationToken ct)
|
||||
@@ -53,14 +53,14 @@ public sealed class PromoteWaitlistedPlayerHandler(
|
||||
if (session is null)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия не найдена.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Сессия не найдена.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.CanManage)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Только owner или co-GM может поднимать игроков из листа ожидания.", showAlert: true, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Только owner или co-GM может поднимать игроков из листа ожидания.", ct, showAlert: true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,14 +89,14 @@ public sealed class PromoteWaitlistedPlayerHandler(
|
||||
if (waitlistedParticipants == 0)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Лист ожидания пуст.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Лист ожидания пуст.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SessionCapacityRules.CanPromoteWaitlistedPlayer(session.MaxPlayers, activeParticipants, waitlistedParticipants))
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Нет свободных мест. Увеличьте лимит перед повышением игрока.", showAlert: true, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Нет свободных мест. Увеличьте лимит перед повышением игрока.", ct, showAlert: true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,17 +165,15 @@ public sealed class PromoteWaitlistedPlayerHandler(
|
||||
transactionCommitted = true;
|
||||
|
||||
var view = SessionBatchViewBuilder.Build(session.Title, batchSessions, batchParticipants);
|
||||
var renderResult = TelegramSessionBatchRenderer.Render(view);
|
||||
|
||||
await BatchMessageEditor.EditBatchMessageAsync(
|
||||
bot,
|
||||
chatId: command.ChatId,
|
||||
messageId: session.BatchMessageId ?? command.MessageId,
|
||||
text: renderResult.Text,
|
||||
replyMarkup: renderResult.Markup,
|
||||
var messageId = session.BatchMessageId ?? command.MessageId;
|
||||
await messenger.UpdateScheduleAsync(
|
||||
new PlatformScheduleMessage(
|
||||
TelegramPlatformIds.Group(command.ChatId),
|
||||
view,
|
||||
TelegramPlatformIds.Message(command.ChatId, threadId: null, messageId)),
|
||||
ct);
|
||||
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, $"{promoted.DisplayName} переведен(а) в основной состав.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, $"{promoted.DisplayName} переведен(а) в основной состав.", ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -188,7 +186,10 @@ public sealed class PromoteWaitlistedPlayerHandler(
|
||||
var errorText = transactionCommitted
|
||||
? "Игрок повышен, но не удалось обновить сообщение расписания."
|
||||
: "Ошибка при обновлении листа ожидания.";
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, errorText, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, errorText, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private Task AnswerAsync(string callbackQueryId, string text, CancellationToken ct, bool showAlert = false) =>
|
||||
messenger.AnswerInteractionAsync(new PlatformInteractionReply(callbackQueryId, text, showAlert), ct);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Text;
|
||||
using Dapper;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Platform;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
|
||||
namespace GmRelay.Bot.Features.Sessions.ExportCalendar;
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed record CalendarSessionDto(Guid Id, string Title, DateTime Schedu
|
||||
|
||||
public sealed class ExportCalendarHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient botClient,
|
||||
IPlatformMessenger messenger,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
public async Task HandleAsync(Message message, CancellationToken cancellationToken)
|
||||
@@ -34,10 +34,10 @@ public sealed class ExportCalendarHandler(
|
||||
|
||||
if (sessionsList.Count == 0)
|
||||
{
|
||||
await botClient.SendMessage(
|
||||
chatId: message.Chat.Id,
|
||||
text: "📭 У этой группы нет запланированных сессий для экспорта.",
|
||||
cancellationToken: cancellationToken);
|
||||
await messenger.SendGroupMessageAsync(
|
||||
TelegramPlatformIds.Group(message.Chat.Id, message.MessageThreadId),
|
||||
"📭 У этой группы нет запланированных сессий для экспорта.",
|
||||
cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,9 +63,7 @@ public sealed class ExportCalendarHandler(
|
||||
sb.AppendLine("END:VCALENDAR");
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
using var stream = new MemoryStream(bytes);
|
||||
|
||||
var inputFile = InputFile.FromStream(stream, "schedule.ics");
|
||||
|
||||
// Create calendar subscription
|
||||
string? subscriptionUrl = null;
|
||||
@@ -93,20 +91,23 @@ public sealed class ExportCalendarHandler(
|
||||
}
|
||||
}
|
||||
|
||||
var replyMarkup = subscriptionUrl is not null
|
||||
? new InlineKeyboardMarkup(new[]
|
||||
var actions = subscriptionUrl is not null
|
||||
? new[]
|
||||
{
|
||||
new[] { InlineKeyboardButton.WithUrl("🔗 Подписаться на календарь", subscriptionUrl) }
|
||||
})
|
||||
: null;
|
||||
new PlatformMessageAction(
|
||||
"calendar-subscription",
|
||||
"🔗 Подписаться на календарь",
|
||||
subscriptionUrl)
|
||||
}
|
||||
: Array.Empty<PlatformMessageAction>();
|
||||
|
||||
await botClient.SendDocument(
|
||||
chatId: message.Chat.Id,
|
||||
document: inputFile,
|
||||
caption: "📅 <b>Ваш календарь игр!</b>\nОткройте файл на устройстве, чтобы добавить события в свой календарь.",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
replyMarkup: replyMarkup,
|
||||
messageThreadId: message.MessageThreadId,
|
||||
cancellationToken: cancellationToken);
|
||||
await messenger.SendCalendarFileAsync(
|
||||
new PlatformCalendarFile(
|
||||
TelegramPlatformIds.Group(message.Chat.Id, message.MessageThreadId),
|
||||
"schedule.ics",
|
||||
bytes,
|
||||
"📅 <b>Ваш календарь игр!</b>\nОткройте файл на устройстве, чтобы добавить события в свой календарь.",
|
||||
actions),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-19
@@ -1,6 +1,7 @@
|
||||
using Dapper;
|
||||
using GmRelay.Bot.Features.Notifications;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Platform;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
@@ -33,6 +34,7 @@ internal sealed record VoteParticipantDto(
|
||||
public sealed class HandleRescheduleTimeInputHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient bot,
|
||||
IPlatformMessenger messenger,
|
||||
DirectSessionNotificationSender directSender,
|
||||
ILogger<HandleRescheduleTimeInputHandler> logger)
|
||||
{
|
||||
@@ -83,12 +85,10 @@ public sealed class HandleRescheduleTimeInputHandler(
|
||||
// 2. Parse voting input
|
||||
if (!RescheduleVotingInput.TryParse(text, DateTimeOffset.UtcNow, out var votingInput, out var parseError))
|
||||
{
|
||||
await bot.SendMessage(
|
||||
chatId: chatId,
|
||||
messageThreadId: proposal.ThreadId,
|
||||
text: $"⚠️ {parseError}\n\nИспользуйте формат:\n<code>25.04.2026 19:30\n26.04.2026 18:00\nДедлайн: 25.04.2026 12:00</code>",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
await messenger.SendGroupMessageAsync(
|
||||
TelegramPlatformIds.Group(chatId, proposal.ThreadId),
|
||||
$"⚠️ {parseError}\n\nИспользуйте формат:\n<code>25.04.2026 19:30\n26.04.2026 18:00\nДедлайн: 25.04.2026 12:00</code>",
|
||||
ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -243,12 +243,10 @@ public sealed class HandleRescheduleTimeInputHandler(
|
||||
|
||||
await transaction.CommitAsync(ct);
|
||||
|
||||
await bot.SendMessage(
|
||||
chatId: chatId,
|
||||
messageThreadId: proposal.ThreadId,
|
||||
text: $"✅ Сессия «{proposal.Title}» перенесена!\n\n📅 Новое время: <b>{newTime.ToOffset(TimeSpan.FromHours(3)).ToString("d MMMM yyyy, HH:mm", System.Globalization.CultureInfo.GetCultureInfo("ru-RU"))}</b> (МСК)\n\n<i>Участников нет — голосование не требуется.</i>",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
await messenger.SendGroupMessageAsync(
|
||||
TelegramPlatformIds.Group(chatId, proposal.ThreadId),
|
||||
$"✅ Сессия «{proposal.Title}» перенесена!\n\n📅 Новое время: <b>{newTime.ToOffset(TimeSpan.FromHours(3)).ToString("d MMMM yyyy, HH:mm", System.Globalization.CultureInfo.GetCultureInfo("ru-RU"))}</b> (МСК)\n\n<i>Участников нет — голосование не требуется.</i>",
|
||||
ct);
|
||||
|
||||
// Re-render batch message with updated time
|
||||
await TryUpdateBatchMessage(proposal, ct);
|
||||
@@ -383,14 +381,12 @@ public sealed class HandleRescheduleTimeInputHandler(
|
||||
if (proposal.BatchMessageId.HasValue)
|
||||
{
|
||||
var view = SessionBatchViewBuilder.Build(proposal.Title, batchSessions, batchParticipants);
|
||||
var renderResult = TelegramSessionBatchRenderer.Render(view);
|
||||
|
||||
await BatchMessageEditor.EditBatchMessageAsync(
|
||||
bot,
|
||||
chatId: proposal.TelegramChatId,
|
||||
messageId: proposal.BatchMessageId.Value,
|
||||
text: renderResult.Text,
|
||||
replyMarkup: renderResult.Markup,
|
||||
await messenger.UpdateScheduleAsync(
|
||||
new PlatformScheduleMessage(
|
||||
TelegramPlatformIds.Group(proposal.TelegramChatId, proposal.ThreadId),
|
||||
view,
|
||||
TelegramPlatformIds.Message(proposal.TelegramChatId, proposal.ThreadId, proposal.BatchMessageId.Value)),
|
||||
ct);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Dapper;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Platform;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
|
||||
@@ -22,6 +23,7 @@ internal sealed record VoteProposalDto(
|
||||
public sealed class HandleRescheduleVoteHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient bot,
|
||||
IPlatformMessenger messenger,
|
||||
ILogger<HandleRescheduleVoteHandler> logger)
|
||||
{
|
||||
public async Task HandleAsync(HandleRescheduleVoteCommand command, CancellationToken ct)
|
||||
@@ -46,20 +48,13 @@ public sealed class HandleRescheduleVoteHandler(
|
||||
|
||||
if (proposal is null)
|
||||
{
|
||||
await bot.AnswerCallbackQuery(
|
||||
command.CallbackQueryId,
|
||||
"Голосование уже завершено или не найдено.",
|
||||
cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Голосование уже завершено или не найдено.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (proposal.VotingDeadlineAt <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
await bot.AnswerCallbackQuery(
|
||||
command.CallbackQueryId,
|
||||
"Дедлайн уже прошёл. Результаты скоро будут применены.",
|
||||
showAlert: true,
|
||||
cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Дедлайн уже прошёл. Результаты скоро будут применены.", ct, showAlert: true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,10 +73,7 @@ public sealed class HandleRescheduleVoteHandler(
|
||||
|
||||
if (playerId is null)
|
||||
{
|
||||
await bot.AnswerCallbackQuery(
|
||||
command.CallbackQueryId,
|
||||
"Вы не являетесь участником этой сессии.",
|
||||
cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Вы не являетесь участником этой сессии.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,9 +161,9 @@ public sealed class HandleRescheduleVoteHandler(
|
||||
logger.LogWarning(ex, "Failed to update reschedule vote message for proposal {ProposalId}", proposal.Id);
|
||||
}
|
||||
|
||||
await bot.AnswerCallbackQuery(
|
||||
command.CallbackQueryId,
|
||||
"Ваш голос учтён. До дедлайна его можно изменить.",
|
||||
cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Ваш голос учтён. До дедлайна его можно изменить.", ct);
|
||||
}
|
||||
|
||||
private Task AnswerAsync(string callbackQueryId, string text, CancellationToken ct, bool showAlert = false) =>
|
||||
messenger.AnswerInteractionAsync(new PlatformInteractionReply(callbackQueryId, text, showAlert), ct);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Dapper;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Platform;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
|
||||
namespace GmRelay.Bot.Features.Sessions.RescheduleSession;
|
||||
|
||||
@@ -28,7 +29,7 @@ internal sealed record RescheduleSessionInfoDto(string Title, bool CanManage);
|
||||
/// </summary>
|
||||
public sealed class InitiateRescheduleHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient bot,
|
||||
IPlatformMessenger messenger,
|
||||
ILogger<InitiateRescheduleHandler> logger)
|
||||
{
|
||||
public async Task HandleAsync(InitiateRescheduleCommand command, CancellationToken ct)
|
||||
@@ -53,14 +54,13 @@ public sealed class InitiateRescheduleHandler(
|
||||
|
||||
if (session is null)
|
||||
{
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия не найдена.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Сессия не найдена.", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.CanManage)
|
||||
{
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId,
|
||||
"Только owner или co-GM может переносить сессию.", showAlert: true, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Только owner или co-GM может переносить сессию.", ct, showAlert: true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,8 +76,7 @@ public sealed class InitiateRescheduleHandler(
|
||||
|
||||
if (hasActive)
|
||||
{
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId,
|
||||
"Уже есть активный запрос на перенос этой сессии.", showAlert: true, cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Уже есть активный запрос на перенос этой сессии.", ct, showAlert: true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,23 +91,23 @@ public sealed class InitiateRescheduleHandler(
|
||||
logger.LogInformation("Reschedule initiated for session {SessionId} by GM {GmId}", command.SessionId, command.TelegramUserId);
|
||||
|
||||
// 4. Prompt GM in chat
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId,
|
||||
"Введите 2-3 варианта времени и дедлайн голосования.", cancellationToken: ct);
|
||||
await AnswerAsync(command.CallbackQueryId, "Введите 2-3 варианта времени и дедлайн голосования.", ct);
|
||||
|
||||
await bot.SendMessage(
|
||||
chatId: command.ChatId,
|
||||
messageThreadId: command.MessageThreadId,
|
||||
text: $"""
|
||||
⏰ Укажите 2-3 варианта времени для сессии «{session.Title}» и дедлайн голосования.
|
||||
|
||||
Формат:
|
||||
<code>25.04.2026 19:30
|
||||
26.04.2026 18:00
|
||||
Дедлайн: 25.04.2026 12:00</code>
|
||||
|
||||
Дедлайн должен быть в будущем и раньше первого предложенного времени.
|
||||
""",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
await messenger.SendGroupMessageAsync(
|
||||
TelegramPlatformIds.Group(command.ChatId, command.MessageThreadId),
|
||||
$"""
|
||||
⏰ Укажите 2-3 варианта времени для сессии «{session.Title}» и дедлайн голосования.
|
||||
|
||||
Формат:
|
||||
<code>25.04.2026 19:30
|
||||
26.04.2026 18:00
|
||||
Дедлайн: 25.04.2026 12:00</code>
|
||||
|
||||
Дедлайн должен быть в будущем и раньше первого предложенного времени.
|
||||
""",
|
||||
ct);
|
||||
}
|
||||
|
||||
private Task AnswerAsync(string callbackQueryId, string text, CancellationToken ct, bool showAlert = false) =>
|
||||
messenger.AnswerInteractionAsync(new PlatformInteractionReply(callbackQueryId, text, showAlert), ct);
|
||||
}
|
||||
|
||||
+11
-13
@@ -2,6 +2,7 @@ using Dapper;
|
||||
using GmRelay.Bot.Features.Notifications;
|
||||
using GmRelay.Bot.Infrastructure.Scheduling;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Platform;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
@@ -26,6 +27,7 @@ internal sealed record DueRescheduleProposalDto(
|
||||
public sealed class RescheduleVotingDeadlineService(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient bot,
|
||||
IPlatformMessenger messenger,
|
||||
DirectSessionNotificationSender directSender,
|
||||
ISystemClock clock,
|
||||
ILogger<RescheduleVotingDeadlineService> logger) : BackgroundService
|
||||
@@ -312,24 +314,20 @@ public sealed class RescheduleVotingDeadlineService(
|
||||
if (proposal.BatchMessageId.HasValue)
|
||||
{
|
||||
var view = SessionBatchViewBuilder.Build(proposal.Title, batchSessions, batchParticipants);
|
||||
var renderResult = TelegramSessionBatchRenderer.Render(view);
|
||||
|
||||
await BatchMessageEditor.EditBatchMessageAsync(
|
||||
bot,
|
||||
chatId: proposal.TelegramChatId,
|
||||
messageId: proposal.BatchMessageId.Value,
|
||||
text: renderResult.Text,
|
||||
replyMarkup: renderResult.Markup,
|
||||
await messenger.UpdateScheduleAsync(
|
||||
new PlatformScheduleMessage(
|
||||
TelegramPlatformIds.Group(proposal.TelegramChatId, proposal.ThreadId),
|
||||
view,
|
||||
TelegramPlatformIds.Message(proposal.TelegramChatId, proposal.ThreadId, proposal.BatchMessageId.Value)),
|
||||
ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await bot.SendMessage(
|
||||
chatId: proposal.TelegramChatId,
|
||||
messageThreadId: proposal.ThreadId,
|
||||
text: $"📣 Расписание обновлено после голосования за перенос сессии «{System.Net.WebUtility.HtmlEncode(proposal.Title)}».",
|
||||
parseMode: ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
await messenger.SendGroupMessageAsync(
|
||||
TelegramPlatformIds.Group(proposal.TelegramChatId, proposal.ThreadId),
|
||||
$"📣 Расписание обновлено после голосования за перенос сессии «{System.Net.WebUtility.HtmlEncode(proposal.Title)}».",
|
||||
ct);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Globalization;
|
||||
using GmRelay.Shared.Platform;
|
||||
|
||||
namespace GmRelay.Bot.Infrastructure.Telegram;
|
||||
|
||||
internal static class TelegramPlatformIds
|
||||
{
|
||||
public static PlatformGroup Group(long chatId, int? threadId = null, string? displayName = null) =>
|
||||
new(
|
||||
PlatformKind.Telegram,
|
||||
chatId.ToString(CultureInfo.InvariantCulture),
|
||||
displayName ?? "Telegram chat",
|
||||
ExternalChannelId: chatId.ToString(CultureInfo.InvariantCulture),
|
||||
ExternalThreadId: threadId?.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
public static PlatformUser User(long telegramId, string displayName, string? username = null) =>
|
||||
new(
|
||||
PlatformKind.Telegram,
|
||||
telegramId.ToString(CultureInfo.InvariantCulture),
|
||||
displayName,
|
||||
username);
|
||||
|
||||
public static PlatformMessageRef Message(long chatId, int? threadId, int messageId) =>
|
||||
new(
|
||||
PlatformKind.Telegram,
|
||||
chatId.ToString(CultureInfo.InvariantCulture),
|
||||
threadId?.ToString(CultureInfo.InvariantCulture),
|
||||
messageId.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
using System.Globalization;
|
||||
using GmRelay.Shared.Platform;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
|
||||
namespace GmRelay.Bot.Infrastructure.Telegram;
|
||||
|
||||
public sealed class TelegramPlatformMessenger(
|
||||
ITelegramBotClient bot,
|
||||
ILogger<TelegramPlatformMessenger> logger) : IPlatformMessenger
|
||||
{
|
||||
public async Task<PlatformMessageRef> SendScheduleAsync(PlatformScheduleMessage message, CancellationToken ct)
|
||||
{
|
||||
EnsureTelegram(message.Group.Platform);
|
||||
|
||||
var chatId = ParseLong(message.Group.ExternalGroupId);
|
||||
var threadId = ParseNullableInt(message.Group.ExternalThreadId);
|
||||
var renderResult = TelegramSessionBatchRenderer.Render(message.View);
|
||||
Message sentMessage;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(message.ImageReference) && renderResult.Text.Length <= 1024)
|
||||
{
|
||||
try
|
||||
{
|
||||
sentMessage = await bot.SendPhoto(
|
||||
chatId: chatId,
|
||||
messageThreadId: threadId,
|
||||
photo: InputFile.FromString(message.ImageReference),
|
||||
caption: renderResult.Text,
|
||||
parseMode: ParseMode.Html,
|
||||
replyMarkup: renderResult.Markup,
|
||||
cancellationToken: ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to send Telegram schedule image for group {ExternalGroupId}", message.Group.ExternalGroupId);
|
||||
sentMessage = await SendScheduleTextMessage(chatId, threadId, renderResult.Text, renderResult.Markup, ct);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message.ImageReference))
|
||||
{
|
||||
await TrySendScheduleImageOnly(chatId, threadId, message.View.Title, message.ImageReference, ct);
|
||||
}
|
||||
|
||||
sentMessage = await SendScheduleTextMessage(chatId, threadId, renderResult.Text, renderResult.Markup, ct);
|
||||
}
|
||||
|
||||
return new PlatformMessageRef(
|
||||
PlatformKind.Telegram,
|
||||
message.Group.ExternalGroupId,
|
||||
message.Group.ExternalThreadId,
|
||||
sentMessage.MessageId.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public async Task UpdateScheduleAsync(PlatformScheduleMessage message, CancellationToken ct)
|
||||
{
|
||||
EnsureTelegram(message.Group.Platform);
|
||||
if (message.ExistingMessage is null)
|
||||
{
|
||||
throw new ArgumentException("Existing schedule message reference is required.", nameof(message));
|
||||
}
|
||||
|
||||
var renderResult = TelegramSessionBatchRenderer.Render(message.View);
|
||||
await BatchMessageEditor.EditBatchMessageAsync(
|
||||
bot,
|
||||
chatId: ParseLong(message.Group.ExternalGroupId),
|
||||
messageId: ParseInt(message.ExistingMessage.ExternalMessageId),
|
||||
text: renderResult.Text,
|
||||
replyMarkup: renderResult.Markup,
|
||||
ct);
|
||||
}
|
||||
|
||||
public Task SendGroupMessageAsync(PlatformGroup group, string htmlText, CancellationToken ct)
|
||||
{
|
||||
EnsureTelegram(group.Platform);
|
||||
return bot.SendMessage(
|
||||
chatId: ParseLong(group.ExternalGroupId),
|
||||
messageThreadId: ParseNullableInt(group.ExternalThreadId),
|
||||
text: htmlText,
|
||||
parseMode: ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
}
|
||||
|
||||
public Task SendPrivateMessageAsync(PlatformPrivateMessage message, CancellationToken ct)
|
||||
{
|
||||
EnsureTelegram(message.Recipient.Platform);
|
||||
return bot.SendMessage(
|
||||
chatId: ParseLong(message.Recipient.ExternalUserId),
|
||||
text: message.HtmlText,
|
||||
parseMode: ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
}
|
||||
|
||||
public Task AnswerInteractionAsync(PlatformInteractionReply reply, CancellationToken ct) =>
|
||||
bot.AnswerCallbackQuery(
|
||||
callbackQueryId: reply.InteractionId,
|
||||
text: reply.Text,
|
||||
showAlert: reply.ShowAlert,
|
||||
cancellationToken: ct);
|
||||
|
||||
public async Task SendCalendarFileAsync(PlatformCalendarFile file, CancellationToken ct)
|
||||
{
|
||||
EnsureTelegram(file.Group.Platform);
|
||||
|
||||
using var stream = new MemoryStream(file.Content);
|
||||
await bot.SendDocument(
|
||||
chatId: ParseLong(file.Group.ExternalGroupId),
|
||||
messageThreadId: ParseNullableInt(file.Group.ExternalThreadId),
|
||||
document: InputFile.FromStream(stream, file.FileName),
|
||||
caption: file.CaptionHtml,
|
||||
parseMode: ParseMode.Html,
|
||||
replyMarkup: BuildActionsMarkup(file.Actions),
|
||||
cancellationToken: ct);
|
||||
}
|
||||
|
||||
private async Task<Message> SendScheduleTextMessage(
|
||||
long chatId,
|
||||
int? threadId,
|
||||
string text,
|
||||
InlineKeyboardMarkup markup,
|
||||
CancellationToken ct) =>
|
||||
await bot.SendMessage(
|
||||
chatId: chatId,
|
||||
messageThreadId: threadId,
|
||||
text: text,
|
||||
parseMode: ParseMode.Html,
|
||||
replyMarkup: markup,
|
||||
cancellationToken: ct);
|
||||
|
||||
private async Task TrySendScheduleImageOnly(
|
||||
long chatId,
|
||||
int? threadId,
|
||||
string title,
|
||||
string imageReference,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await bot.SendPhoto(
|
||||
chatId: chatId,
|
||||
messageThreadId: threadId,
|
||||
photo: InputFile.FromString(imageReference),
|
||||
caption: $"🎲 {System.Net.WebUtility.HtmlEncode(title)}",
|
||||
parseMode: ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to send Telegram schedule image for chat {ChatId}", chatId);
|
||||
}
|
||||
}
|
||||
|
||||
private static InlineKeyboardMarkup? BuildActionsMarkup(IReadOnlyList<PlatformMessageAction> actions)
|
||||
{
|
||||
if (actions.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new InlineKeyboardMarkup(
|
||||
actions.Select(action => new[]
|
||||
{
|
||||
Uri.TryCreate(action.Payload, UriKind.Absolute, out var uri) &&
|
||||
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
|
||||
? InlineKeyboardButton.WithUrl(action.Label, action.Payload)
|
||||
: InlineKeyboardButton.WithCallbackData(action.Label, action.Payload)
|
||||
}));
|
||||
}
|
||||
|
||||
private static void EnsureTelegram(PlatformKind platform)
|
||||
{
|
||||
if (platform != PlatformKind.Telegram)
|
||||
{
|
||||
throw new NotSupportedException($"Telegram messenger cannot send messages for platform {platform}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static long ParseLong(string value) => long.Parse(value, CultureInfo.InvariantCulture);
|
||||
|
||||
private static int ParseInt(string value) => int.Parse(value, CultureInfo.InvariantCulture);
|
||||
|
||||
private static int? ParseNullableInt(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : int.Parse(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using GmRelay.Bot.Infrastructure.Health;
|
||||
using GmRelay.Bot.Infrastructure.Logging;
|
||||
using GmRelay.Bot.Infrastructure.Scheduling;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
using GmRelay.Shared.Platform;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
|
||||
@@ -50,6 +51,7 @@ builder.Services.AddSingleton<ITelegramBotClient>(sp =>
|
||||
return new TelegramBotClient(token);
|
||||
});
|
||||
builder.Services.AddSingleton<ITelegramUpdateSource, TelegramUpdateSource>();
|
||||
builder.Services.AddSingleton<IPlatformMessenger, TelegramPlatformMessenger>();
|
||||
|
||||
// ── Feature handlers (explicit registration — AOT safe) ──────────────
|
||||
builder.Services.AddSingleton<SendConfirmationHandler>();
|
||||
|
||||
Reference in New Issue
Block a user