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 logger) : IPlatformMessenger { public async Task 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); var existingMessage = message.ExistingMessage; if (existingMessage is null) { throw new ArgumentException("Existing schedule message reference is required.", nameof(message)); } EnsureTelegram(existingMessage.Platform); if (!string.Equals(message.Group.ExternalGroupId, existingMessage.ExternalGroupId, StringComparison.Ordinal) || !string.Equals(message.Group.ExternalThreadId, existingMessage.ExternalThreadId, StringComparison.Ordinal)) { throw new ArgumentException("Existing schedule message reference must match the schedule group.", nameof(message)); } var renderResult = TelegramSessionBatchRenderer.Render(message.View); await BatchMessageEditor.EditBatchMessageAsync( bot, chatId: ParseLong(existingMessage.ExternalGroupId), messageId: ParseInt(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 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 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); }