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 : IMessangerAdapterSetup { private readonly ILogger _logger; private TelegramBotClient? _client; private string _token; private static Capabilities _capabilities = new() { SupportsInlineButtons = true, SupportsReplyButtons = true, SupportsAlbums = true, SupportsFormattingMarkdown = true, SupportsFormattingHtml = true, MaxMessageLength = 4096, }; /// Создать адаптер Telegram. public TelegramAdapter(ILogger logger, string token) { _logger = logger; _token = token; } /// ///Идентификатор мессенджера / адаптера /// 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); }, 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; } /// public async Task SendTextAsync(string chatId, string text, MessageFormat format = MessageFormat.Plain, IEnumerable>? inline = null, IEnumerable>? reply = null, string? messageId = null, CancellationToken ct = default) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); return null; } InlineKeyboardMarkup? inlineMarkup = null; ReplyMarkup? markup = null; if (inline is not null && inline.Any()) { inlineMarkup = new InlineKeyboardMarkup( inline.Select(row => row.Select(b => new InlineKeyboardButton(b.Label, b.Value)).ToArray()) .ToArray() ); } else if (reply is not null) { if (reply.Any()) { markup = new ReplyKeyboardMarkup( reply.Select(row => row.Select(b => new KeyboardButton(b.Label)).ToArray()).ToArray() ) { ResizeKeyboard = true }; } else { markup = new ReplyKeyboardRemove(); } } var parseMode = ParseMode.None; switch (format) { case MessageFormat.Html: { parseMode = ParseMode.Html; break; } case MessageFormat.Plain: { parseMode = ParseMode.None; break; } case MessageFormat.Markdown: { parseMode = ParseMode.MarkdownV2; break; } default: { _logger.Log(LogLevel.Warn, $"MessageFormat '{format}' not supported. Degraded to plain text."); break; } } // Длина сообщения 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(messageId)) { await _client.EditMessageText( messageId: int.Parse(messageId), chatId: long.Parse(chatId), text: text, parseMode: parseMode, replyMarkup: inlineMarkup, cancellationToken: ct ); return messageId; } else { if (inlineMarkup is not null) markup = inlineMarkup; var message = await _client.SendMessage( chatId: long.Parse(chatId), text: text, parseMode: parseMode, replyMarkup: markup, cancellationToken: ct ); return message.Id.ToString(); } } /// public async Task SendFileAsync(string chatId, FileDescriptor file, string? caption = null, MessageFormat? captionFormat = null, IEnumerable>? inline = null, IEnumerable>? reply = null, CancellationToken ct = default ) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); return; } // Получаем поток, если он задан Stream? stream = null; if (file.GetStreamAsync is not null) { stream = await file.GetStreamAsync(ct); 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 (captionFormat) { case MessageFormat.Html: { parseMode = ParseMode.Html; break; } case MessageFormat.Plain: { parseMode = ParseMode.None; break; } case MessageFormat.Markdown: { parseMode = ParseMode.MarkdownV2; break; } case null: { parseMode = ParseMode.None; break; } default: { _logger.Log(LogLevel.Warn, $"MessageFormat '{captionFormat}' not supported. Degraded to plain text."); break; } } ReplyMarkup? markup = null; if (inline is not null && inline.Any()) { markup = new InlineKeyboardMarkup( inline.Select(row => row.Select(b => new InlineKeyboardButton(b.Label, b.Value)).ToArray()) .ToArray() ); } else if (reply is not null) { if (reply.Any()) { markup = new ReplyKeyboardMarkup( reply.Select(row => row.Select(b => new KeyboardButton(b.Label)).ToArray()).ToArray() ) { ResizeKeyboard = true }; } else { markup = new ReplyKeyboardRemove(); } } // В зависимости от FileKind выбираем подходящий метод switch (file.Kind) { case FileKind.Photo: await _client.SendPhoto(long.Parse(chatId), inputFile, caption ?? "", parseMode, replyMarkup: markup, cancellationToken: ct); break; case FileKind.Video: await _client.SendVideo(long.Parse(chatId), inputFile, caption: caption ?? "", parseMode, replyMarkup: markup, cancellationToken: ct); break; case FileKind.Audio: await _client.SendAudio(long.Parse(chatId), inputFile, caption ?? "", parseMode, replyMarkup: markup, cancellationToken: ct); break; default: await _client.SendDocument(long.Parse(chatId), inputFile, caption ?? "", parseMode, replyMarkup: markup, cancellationToken: ct); break; } } /// public IAlbumBuilder CreateAlbumBuilder(PageContext ctx) => new TelegramAlbumBuilder(this, ctx, _logger, _client); /// public Task OnLeaveAsync(PageContext ctx, CancellationToken ct) => Task.CompletedTask; }