using BotPages.Core; using BotPages.Core.Abstractions; using BotPages.Core.Context; using BotPages.Core.Logging; using BotPages.Core.Messaging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; namespace BotPages.Telegram; /// /// Адаптер для Telegram на базе Telegram.Bot. /// Реализует отправку текста, кнопок, файлов, альбомов и прогресса. /// public sealed class TelegramAdapter : IMessengerAdapterSetup { internal static readonly string AdapterType = typeof(TelegramAdapter).FullName; private readonly ILogger _logger; private TelegramBotClient? _client; private string _token; private readonly TelegramOptions _options; private static Capabilities _capabilities = new() { SupportsInlineButtons = true, SupportsReplyButtons = true, SupportsAlbums = true, SupportsFormattingMarkdown = true, SupportsFormattingHtml = true, MaxMessageLength = 4096, }; /// Создать адаптер Telegram. public TelegramAdapter(ILogger logger, string token, TelegramOptions? options = null) { _logger = logger; _token = token; _options = options ?? new TelegramOptions(); } /// ///Идентификатор мессенджера / адаптера /// public string MessengerType { get; set; } = "Telegram: " + Guid.NewGuid().ToString(); /// /// Доступные возможности адаптера. /// public Capabilities Capabilities => _capabilities; /// /// Запустить polling для приема обновлений от Telegram. /// public async Task StartAdapterAsync(Func onUpdate, List commands, CancellationToken ct) { _client = new TelegramBotClient(_token); _client.StartReceiving( updateHandler: async (_, update, ct2) => { var mapped = TelegramUpdateMapper.Map(MessengerType, update, _client); if (mapped is not null) await onUpdate(mapped); if (update.CallbackQuery is not null) { await _.AnswerCallbackQuery(update.CallbackQuery.Id); } }, errorHandler: async (_, ex, ct2) => { _logger.Log(LogLevel.Warn, $"{MessengerType} error.", ex); await Task.CompletedTask; }, cancellationToken: ct ); await _client.SetMyCommands(commands.Where(t => t.Publish).Select(t => new BotCommand(t.Name, t.Description ?? t.Name.TrimStart('/'))), cancellationToken: ct); var me = await _client.GetMe(); _logger.Log(LogLevel.Info, $"{MessengerType} started: @{me.Username}"); return; } /// /// Универсальный внутренний метод отправки — определяет, нужно ли отправлять текст или файл по параметрам. /// Возвращает id сообщения (или null). /// public async Task SendAsync(SendRequest req, CancellationToken ct) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); return null; } // Определите параметры адаптера (TelegramOptions) из папки или используйте параметры адаптера по умолчанию. TelegramOptions telegramOptions = _options; if (req.AdapterOptions is AdapterOptionsBag bag && bag.TryGet(AdapterType, out TelegramOptions? opt) && opt is not null) { telegramOptions = opt; } var disableNotification = !telegramOptions.NotifyOnSend; // Build markup var inlineMarkup = BuildInlineMarkup(req.Inline); ReplyMarkup? markup = BuildReplyMarkup(req.Reply); // Файлы: сейчас поддерживается один файл через SendRequest.File. // При необходимости для нескольких файлов следует использовать альбомы (CreateAlbumBuilder) if (req.File is not null) { if (inlineMarkup is not null) markup = inlineMarkup; var sent = await SendFileAsync(req.File, req.ChatId, markup, disableNotification, req.Caption, req.CaptionFormat, ct); return sent?.MessageId.ToString(); } // Текст if (!string.IsNullOrWhiteSpace(req.Text)) { return await SendTextAsync(req, inlineMarkup, markup, disableNotification, ct); } return null; } // --- Helpers --- private InlineKeyboardMarkup? BuildInlineMarkup(IEnumerable>? inline) { if (inline is null || !inline.Any()) return null; return new InlineKeyboardMarkup( inline.Select(row => row.Select(b => new InlineKeyboardButton(b.Label, b.Value)).ToArray()).ToArray() ); } private ReplyMarkup? BuildReplyMarkup(IEnumerable>? reply) { if (reply is null) return null; if (reply.Any()) { return new ReplyKeyboardMarkup(reply.Select(row => row.Select(b => new KeyboardButton(b.Label)).ToArray()).ToArray()) { ResizeKeyboard = true }; } else { return new ReplyKeyboardRemove(); } } private static ParseMode GetParseMode(MessageFormat? format) { return format switch { MessageFormat.Html => ParseMode.Html, MessageFormat.Markdown => ParseMode.MarkdownV2, _ => ParseMode.None, }; } private async Task CreateInputFileAsync(FileDescriptor file, CancellationToken ct) { Stream? stream = null; if (file.GetStreamAsync is not null) { stream = await file.GetStreamAsync(ct); if (stream is not null) stream.Position = 0; } if (stream is not null && stream != Stream.Null) { return new InputFileStream(stream, file.Name); } else if (file.Id.StartsWith("http://") || file.Id.StartsWith("https://")) { return new InputFileUrl(file.Id); } else { return new InputFileId(file.Id); } } private async Task SendFileAsync(FileDescriptor file, string chatId, ReplyMarkup? markup, bool disableNotification, string? caption, MessageFormat? captionFormat, CancellationToken ct) { var inputFile = await CreateInputFileAsync(file, ct); var parseMode = GetParseMode(captionFormat); return file.Kind switch { FileKind.Photo => await _client.SendPhoto(long.Parse(chatId), inputFile, caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct), FileKind.Video => await _client.SendVideo(long.Parse(chatId), inputFile, caption: caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct), FileKind.Audio => await _client.SendAudio(long.Parse(chatId), inputFile, caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct), _ => await _client.SendDocument(long.Parse(chatId), inputFile, caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct), }; } private async Task SendTextAsync(SendRequest req, InlineKeyboardMarkup? inlineMarkup, ReplyMarkup? markup, bool disableNotification, CancellationToken ct) { var format = req.TextFormat ?? MessageFormat.Plain; var parseMode = GetParseMode(format); var text = req.Text!; if (text.Length > Capabilities.MaxMessageLength) { _logger.Log(LogLevel.Warn, $"Message too long ({text.Length}). Truncated to {Capabilities.MaxMessageLength}."); text = text.Substring(0, Capabilities.MaxMessageLength); } if (!string.IsNullOrWhiteSpace(req.MessageId)) { await _client.EditMessageText( messageId: int.Parse(req.MessageId), chatId: long.Parse(req.ChatId), text: text, parseMode: parseMode, replyMarkup: inlineMarkup, cancellationToken: ct ); return req.MessageId; } else { if (inlineMarkup is not null) markup = inlineMarkup; var message = await _client.SendMessage( chatId: long.Parse(req.ChatId), text: text, parseMode: parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct ); return message.MessageId.ToString(); } } /// public IAlbumBuilder CreateAlbumBuilder(PageContext ctx) => new TelegramAlbumBuilder(this, ctx, _logger, _client); /// public Task OnLeaveAsync(PageContext ctx, CancellationToken ct) => Task.CompletedTask; }