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(); Id = Guid.NewGuid().ToString(); DisplayName = $"Telegram {Id}"; } /// public string Type => "Telegram"; /// public string Id { get; set; } /// /// Получение отображаемого имени бота (например, "Telegram: @mybot"). Доступно после запуска адаптера. /// public string DisplayName { get; private set; } /// public void SetAdapterId(string adapterId) => Id = adapterId; /// /// Доступные возможности адаптера. /// 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(this, 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, $"{Type} ({Id}) 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(); DisplayName = $"Telegram: @{me.Username}"; _logger.Log(LogLevel.Info, $"{Type} ({Id}) started: {DisplayName}"); return; } /// /// Универсальный внутренний метод отправки — определяет, нужно ли отправлять текст или файл по параметрам. /// Возвращает id сообщения (или null). /// public async Task SendAsync(SendRequest req, CancellationToken ct) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return null; } // Определите параметры адаптера (TelegramOptions) из папки или используйте параметры адаптера по умолчанию. TelegramOptions telegramOptions = _options; if (req.AdapterOptions is AdapterOptionsBag bag && bag.TryGet(Type, out TelegramOptions? opt) && opt is not null) { telegramOptions = opt; } var disableNotification = !telegramOptions.NotifyOnSend || req.DisableNotification; // Build markup var inlineMarkup = BuildInlineMarkup(req.Inline); ReplyMarkup? markup = BuildReplyMarkup(req.Reply); // Ответ на сообщение int? replyToMessageId = null; if (!string.IsNullOrEmpty(req.ReplyToMessageId) && int.TryParse(req.ReplyToMessageId, out var replyId)) { replyToMessageId = replyId; } // Файлы: сейчас поддерживается один файл через 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, replyToMessageId, req.ProtectContent, ct); return sent?.MessageId.ToString(); } // Текст if (!string.IsNullOrWhiteSpace(req.Text)) { return await SendTextAsync(req, inlineMarkup, markup, disableNotification, replyToMessageId, req.DisableWebPagePreview, req.ProtectContent, ct); } return null; } /// public Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return Task.CompletedTask; } return _client.DeleteMessage(chatId, Convert.ToInt32(messageId), ct); } /// public async Task DeleteMultipleAsync(string chatId, IEnumerable messageIds, CancellationToken ct = default) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return false; } try { // Telegram не поддерживает массовое удаление, удаляем по одному foreach (var messageId in messageIds) { await DeleteAsync(chatId, messageId, ct); } return true; } catch (Exception ex) { _logger.Log(LogLevel.Warn, $"Failed to delete multiple messages: {ex.Message}"); return false; } } /// public async Task EditTextAsync(string chatId, string messageId, string text, MessageFormat? format = null, CancellationToken ct = default) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return null; } try { var parseMode = GetParseMode(format); await _client.EditMessageText( chatId: long.Parse(chatId), messageId: int.Parse(messageId), text: text, parseMode: parseMode, cancellationToken: ct ); return messageId; } catch (Exception ex) { _logger.Log(LogLevel.Warn, $"Failed to edit text: {ex.Message}"); return null; } } /// public async Task EditButtonsAsync(string chatId, string messageId, IEnumerable>? inlineButtons = null, CancellationToken ct = default) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return null; } try { var inlineMarkup = BuildInlineMarkup(inlineButtons); await _client.EditMessageReplyMarkup( chatId: long.Parse(chatId), messageId: int.Parse(messageId), replyMarkup: inlineMarkup, cancellationToken: ct ); return messageId; } catch (Exception ex) { _logger.Log(LogLevel.Warn, $"Failed to edit buttons: {ex.Message}"); return null; } } /// public async Task PinMessageAsync(string chatId, string messageId, bool disableNotification = false, CancellationToken ct = default) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return false; } try { await _client.PinChatMessage( chatId: long.Parse(chatId), messageId: int.Parse(messageId), disableNotification: disableNotification, cancellationToken: ct ); return true; } catch (Exception ex) { _logger.Log(LogLevel.Warn, $"Failed to pin message: {ex.Message}"); return false; } } /// public async Task UnpinMessageAsync(string chatId, string messageId, CancellationToken ct = default) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return false; } try { await _client.UnpinChatMessage( chatId: long.Parse(chatId), messageId: int.Parse(messageId), cancellationToken: ct ); return true; } catch (Exception ex) { _logger.Log(LogLevel.Warn, $"Failed to unpin message: {ex.Message}"); return false; } } /// public async Task GetMessageInfoAsync(string chatId, string messageId, CancellationToken ct = default) { throw new NotImplementedException(); } /// public async Task ForwardMessageAsync(string fromChatId, string messageId, string toChatId, bool disableNotification = false, CancellationToken ct = default) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return null; } try { var message = await _client.ForwardMessage( chatId: long.Parse(toChatId), fromChatId: long.Parse(fromChatId), messageId: int.Parse(messageId), disableNotification: disableNotification, cancellationToken: ct ); return message.MessageId.ToString(); } catch (Exception ex) { _logger.Log(LogLevel.Warn, $"Failed to forward message: {ex.Message}"); return null; } } /// public async Task CopyMessageAsync(string fromChatId, string messageId, string toChatId, string? caption = null, MessageFormat? captionFormat = null, bool disableNotification = false, CancellationToken ct = default) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return null; } try { var parseMode = GetParseMode(captionFormat); var message = await _client.CopyMessage( chatId: long.Parse(toChatId), fromChatId: long.Parse(fromChatId), messageId: int.Parse(messageId), caption: caption, parseMode: parseMode, disableNotification: disableNotification, cancellationToken: ct ); return message.Id.ToString(); } catch (Exception ex) { _logger.Log(LogLevel.Warn, $"Failed to copy message: {ex.Message}"); 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) { Style = b.Style switch { ButtonStyle.Default => null, ButtonStyle.Info => KeyboardButtonStyle.Primary, ButtonStyle.Error => KeyboardButtonStyle.Danger, ButtonStyle.Success => KeyboardButtonStyle.Success, _ => null, } }).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) { Style = b.Style switch { ButtonStyle.Default => null, ButtonStyle.Info => KeyboardButtonStyle.Primary, ButtonStyle.Error => KeyboardButtonStyle.Danger, ButtonStyle.Success => KeyboardButtonStyle.Success, _ => null, } }).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, MessageFormat.Plain => ParseMode.None, _ => 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, int? replyToMessageId, bool protectContent, CancellationToken ct) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return null; } 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, replyParameters: replyToMessageId, replyMarkup: markup, disableNotification: disableNotification, protectContent: protectContent, cancellationToken: ct), FileKind.Video => await _client.SendVideo( long.Parse(chatId), inputFile, caption: caption ?? "", parseMode, replyParameters: replyToMessageId, replyMarkup: markup, disableNotification: disableNotification, protectContent: protectContent, cancellationToken: ct), FileKind.Audio => await _client.SendAudio( long.Parse(chatId), inputFile, caption ?? "", parseMode, replyParameters: replyToMessageId, replyMarkup: markup, disableNotification: disableNotification, protectContent: protectContent, cancellationToken: ct), _ => await _client.SendDocument( long.Parse(chatId), inputFile, caption ?? "", parseMode, replyParameters: replyToMessageId, replyMarkup: markup, disableNotification: disableNotification, protectContent: protectContent, cancellationToken: ct), }; } private async Task SendTextAsync(SendRequest req, InlineKeyboardMarkup? inlineMarkup, ReplyMarkup? markup, bool disableNotification, int? replyToMessageId, bool disableWebPagePreview, bool protectContent, CancellationToken ct) { if (_client is null) { _logger.Log(LogLevel.Critical, $"{Type} client is not initialized."); return null; } 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, linkPreviewOptions: new() { IsDisabled = disableWebPagePreview, }, 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, linkPreviewOptions: new() { IsDisabled = disableWebPagePreview, }, replyMarkup: markup, replyParameters: replyToMessageId, disableNotification: disableNotification, protectContent: protectContent, 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; }