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;
}