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 { 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; } // determine adapter-specific options (TelegramOptions) from bag or use adapter default TelegramOptions telegramOptions = _options; if (req.AdapterOptions is AdapterOptionsBag bag && bag.TryGet("telegram", out TelegramOptions? opt) && opt is not null) { telegramOptions = opt; } var disableNotification = !telegramOptions.NotifyOnSend; // Build markup InlineKeyboardMarkup? inlineMarkup = null; ReplyMarkup? markup = null; if (req.Inline is not null && req.Inline.Any()) { inlineMarkup = new InlineKeyboardMarkup( req.Inline.Select(row => row.Select(b => new InlineKeyboardButton(b.Label, b.Value)).ToArray()).ToArray() ); } else if (req.Reply is not null) { if (req.Reply.Any()) { markup = new ReplyKeyboardMarkup(req.Reply.Select(row => row.Select(b => new KeyboardButton(b.Label)).ToArray()).ToArray()) { ResizeKeyboard = true }; } else { markup = new ReplyKeyboardRemove(); } } // If there is a file — send file if (req.File is not null) { var file = req.File; // Получаем поток, если он задан Stream? stream = null; if (file.GetStreamAsync is not null) { stream = await file.GetStreamAsync(ct); if (stream is not null) stream.Position = 0; } InputFile inputFile; if (stream is not null && stream != Stream.Null) { inputFile = new InputFileStream(stream, file.Name); } else if (file.Id.StartsWith("http://") || file.Id.StartsWith("https://")) { inputFile = new InputFileUrl(file.Id); } else { inputFile = new InputFileId(file.Id); } var parseMode = ParseMode.None; switch (req.CaptionFormat) { case MessageFormat.Html: parseMode = ParseMode.Html; break; case MessageFormat.Markdown: parseMode = ParseMode.MarkdownV2; break; case MessageFormat.Plain: case null: parseMode = ParseMode.None; break; } if (inlineMarkup is not null) markup = inlineMarkup; Message? sentMessage = req.File.Kind switch { FileKind.Photo => await _client.SendPhoto(long.Parse(req.ChatId), inputFile, req.Caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct), FileKind.Video => await _client.SendVideo(long.Parse(req.ChatId), inputFile, caption: req.Caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct), FileKind.Audio => await _client.SendAudio(long.Parse(req.ChatId), inputFile, req.Caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct), _ => await _client.SendDocument(long.Parse(req.ChatId), inputFile, req.Caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct), }; return sentMessage?.MessageId.ToString(); } // Otherwise treat as text if (!string.IsNullOrWhiteSpace(req.Text)) { var format = req.TextFormat ?? MessageFormat.Plain; var parseMode = ParseMode.None; switch (format) { case MessageFormat.Html: parseMode = ParseMode.Html; break; case MessageFormat.Markdown: parseMode = ParseMode.MarkdownV2; break; case MessageFormat.Plain: default: parseMode = ParseMode.None; break; } // Длина сообщения 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(); } } return null; } /// public Task SendTextAsync(string chatId, string text, MessageFormat format = MessageFormat.Plain, IEnumerable>? inline = null, IEnumerable>? reply = null, string? messageId = null, object? adapterOptions = null, CancellationToken ct = default) { var req = new SendRequest { ChatId = chatId, Text = text, TextFormat = format, Inline = inline, Reply = reply, MessageId = messageId, AdapterOptions = adapterOptions as AdapterOptionsBag }; return SendAsync(req, ct); } /// public Task SendFileAsync(string chatId, FileDescriptor file, string? caption = null, MessageFormat? captionFormat = null, IEnumerable>? inline = null, IEnumerable>? reply = null, object? adapterOptions = null, CancellationToken ct = default ) { var req = new SendRequest { ChatId = chatId, File = file, Caption = caption, CaptionFormat = captionFormat, Inline = inline, Reply = reply, AdapterOptions = adapterOptions as AdapterOptionsBag }; return SendAsync(req, ct); } /// public IAlbumBuilder CreateAlbumBuilder(PageContext ctx) => new TelegramAlbumBuilder(this, ctx, _logger, _client); /// public Task OnLeaveAsync(PageContext ctx, CancellationToken ct) => Task.CompletedTask; }