314 lines
10 KiB
C#
314 lines
10 KiB
C#
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;
|
||
|
||
|
||
/// <summary>
|
||
/// Адаптер для Telegram на базе Telegram.Bot.
|
||
/// Реализует отправку текста, кнопок, файлов, альбомов и прогресса.
|
||
/// </summary>
|
||
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,
|
||
};
|
||
|
||
/// <summary>Создать адаптер Telegram.</summary>
|
||
public TelegramAdapter(ILogger logger, string token)
|
||
{
|
||
_logger = logger;
|
||
_token = token;
|
||
}
|
||
|
||
/// <summary>
|
||
///Идентификатор мессенджера / адаптера
|
||
/// </summary>
|
||
public string MessengerType { get; set; } = "Telegram: " + Guid.NewGuid().ToString();
|
||
|
||
/// <summary>
|
||
/// Доступные возможности адаптера.
|
||
/// </summary>
|
||
public Capabilities Capabilities => _capabilities;
|
||
|
||
/// <summary>
|
||
/// Запустить polling для приема обновлений от Telegram.
|
||
/// </summary>
|
||
public async Task StartAdapterAsync(Func<UpdateContext, Task> onUpdate, List<BotPages.Core.Routing.Command> 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;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<string?> SendTextAsync(string chatId, string text,
|
||
MessageFormat format = MessageFormat.Plain,
|
||
IEnumerable<IEnumerable<InlineButton>>? inline = null,
|
||
IEnumerable<IEnumerable<ReplyButton>>? 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();
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task SendFileAsync(string chatId,
|
||
FileDescriptor file,
|
||
string? caption = null,
|
||
MessageFormat? captionFormat = null,
|
||
IEnumerable<IEnumerable<InlineButton>>? inline = null,
|
||
IEnumerable<IEnumerable<ReplyButton>>? 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;
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public IAlbumBuilder CreateAlbumBuilder(PageContext ctx) => new TelegramAlbumBuilder(this, ctx, _logger, _client);
|
||
|
||
/// <inheritdoc />
|
||
public Task OnLeaveAsync(PageContext ctx, CancellationToken ct) => Task.CompletedTask;
|
||
} |