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 : MessengerAdapterBase
{
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();
// Устанавливаем имя для отображения
DisplayName = $"Telegram Bot";
}
/// Тип адаптера.
public override string AdapterType => "Telegram";
///
/// Идентификатор мессенджера / адаптера
///
public string MessengerType => "Telegram";
///
/// Доступные возможности адаптера.
///
public override Capabilities Capabilities => _capabilities;
///
/// Запустить polling для приема обновлений от Telegram.
///
public override 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, $"{AdapterType} ({AdapterId}) 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, $"{AdapterType} ({AdapterId}) started: {DisplayName}");
return;
}
///
/// Универсальный внутренний метод отправки — определяет, нужно ли отправлять текст или файл по параметрам.
/// Возвращает id сообщения (или null).
///
public override 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 || 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 override Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default)
{
if (_client is null)
{
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized.");
return Task.CompletedTask;
}
return _client.DeleteMessage(chatId, Convert.ToInt32(messageId), ct);
}
///
public override async Task DeleteMultipleAsync(string chatId, IEnumerable messageIds, CancellationToken ct = default)
{
if (_client is null)
{
_logger.Log(LogLevel.Critical, $"{MessengerType} 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 override async Task EditTextAsync(string chatId, string messageId, string text,
MessageFormat? format = null, CancellationToken ct = default)
{
if (_client is null)
{
_logger.Log(LogLevel.Critical, $"{MessengerType} 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 override async Task EditButtonsAsync(string chatId, string messageId,
IEnumerable>? inlineButtons = null, CancellationToken ct = default)
{
if (_client is null)
{
_logger.Log(LogLevel.Critical, $"{MessengerType} 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 override async Task PinMessageAsync(string chatId, string messageId, bool disableNotification = false,
CancellationToken ct = default)
{
if (_client is null)
{
_logger.Log(LogLevel.Critical, $"{MessengerType} 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 override async Task UnpinMessageAsync(string chatId, string messageId, CancellationToken ct = default)
{
if (_client is null)
{
_logger.Log(LogLevel.Critical, $"{MessengerType} 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 override async Task GetMessageInfoAsync(string chatId, string messageId, CancellationToken ct = default)
{
throw new NotImplementedException();
}
///
public override async Task ForwardMessageAsync(string fromChatId, string messageId, string toChatId,
bool disableNotification = false, CancellationToken ct = default)
{
if (_client is null)
{
_logger.Log(LogLevel.Critical, $"{MessengerType} 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 override 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, $"{MessengerType} 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)).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, int? replyToMessageId,
bool protectContent, 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,
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)
{
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 override IAlbumBuilder CreateAlbumBuilder(PageContext ctx) => new TelegramAlbumBuilder(this, ctx, _logger, _client);
///
public override Task OnLeaveAsync(PageContext ctx, CancellationToken ct) => Task.CompletedTask;
}