Новый api отправки сообщений
This commit is contained in:
@@ -15,8 +15,14 @@ public static class BotPagesAppExtension
|
||||
/// <param name="messengerType"></param>
|
||||
/// <returns></returns>
|
||||
public static BotPagesApp AddTelegramAdapter(this BotPagesApp app, string token, string messengerType = "")
|
||||
=> app.AddTelegramAdapter(token, null, messengerType);
|
||||
|
||||
/// <summary>
|
||||
/// Добавление адаптера для телеграмм с опциями.
|
||||
/// </summary>
|
||||
public static BotPagesApp AddTelegramAdapter(this BotPagesApp app, string token, TelegramOptions? options, string messengerType = "")
|
||||
{
|
||||
var telegram = new TelegramAdapter(app.Logger, token);
|
||||
var telegram = new TelegramAdapter(app.Logger, token, options);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(messengerType)) telegram.MessengerType = messengerType;
|
||||
|
||||
|
||||
43
BotPages.Telegram/MessageBuilderExtensions.cs
Normal file
43
BotPages.Telegram/MessageBuilderExtensions.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using BotPages.Core.Abstractions;
|
||||
using BotPages.Core.Messaging;
|
||||
|
||||
namespace BotPages.Telegram;
|
||||
|
||||
/// <summary>
|
||||
/// Ðàñøèðåíèÿ äëÿ `MessageBuilder` ñïåöèôè÷íûå äëÿ Telegram.
|
||||
/// Ïîçâîëÿþò óäîáíî çàäàòü `TelegramOptions` äëÿ êîíêðåòíîãî ñîîáùåíèÿ
|
||||
/// áåç èçìåíåíèÿ ñàìîãî áèëäåðà.
|
||||
/// </summary>
|
||||
public static class MessageBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Óñòàíîâèòü îïöèè Telegram äëÿ êîíêðåòíîãî ñîîáùåíèÿ (ïåðåîïðåäåëÿþò îïöèè àäàïòåðà).
|
||||
/// </summary>
|
||||
public static MessageBuilder WithTelegramOptions(this MessageBuilder builder, TelegramOptions options)
|
||||
{
|
||||
// Ensure bag exists
|
||||
var bag = (builder as object) switch
|
||||
{
|
||||
MessageBuilder mb => GetAdapterBag(mb) ?? CreateAndSetAdapterBag(mb),
|
||||
_ => null
|
||||
};
|
||||
|
||||
bag?.Set("telegram", options);
|
||||
return builder;
|
||||
}
|
||||
|
||||
// Reflection helpers to access private adapterOptions field on MessageBuilder
|
||||
private static AdapterOptionsBag? GetAdapterBag(MessageBuilder builder)
|
||||
{
|
||||
var fi = typeof(MessageBuilder).GetField("_adapterOptions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
return fi?.GetValue(builder) as AdapterOptionsBag;
|
||||
}
|
||||
|
||||
private static AdapterOptionsBag CreateAndSetAdapterBag(MessageBuilder builder)
|
||||
{
|
||||
var fi = typeof(MessageBuilder).GetField("_adapterOptions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var bag = new AdapterOptionsBag();
|
||||
fi?.SetValue(builder, bag);
|
||||
return bag;
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,12 @@ namespace BotPages.Telegram;
|
||||
/// Адаптер для Telegram на базе Telegram.Bot.
|
||||
/// Реализует отправку текста, кнопок, файлов, альбомов и прогресса.
|
||||
/// </summary>
|
||||
public sealed class TelegramAdapter : IMessangerAdapterSetup
|
||||
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,
|
||||
@@ -37,10 +38,11 @@ public sealed class TelegramAdapter : IMessangerAdapterSetup
|
||||
};
|
||||
|
||||
/// <summary>Создать адаптер Telegram.</summary>
|
||||
public TelegramAdapter(ILogger logger, string token)
|
||||
public TelegramAdapter(ILogger logger, string token, TelegramOptions? options = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_token = token;
|
||||
_options = options ?? new TelegramOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -81,6 +83,7 @@ public sealed class TelegramAdapter : IMessangerAdapterSetup
|
||||
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();
|
||||
@@ -89,13 +92,11 @@ public sealed class TelegramAdapter : IMessangerAdapterSetup
|
||||
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)
|
||||
/// <summary>
|
||||
/// Универсальный внутренний метод отправки — определяет, нужно ли отправлять текст или файл по параметрам.
|
||||
/// Возвращает id сообщения (или null).
|
||||
/// </summary>
|
||||
public async Task<string?> SendAsync(SendRequest req, CancellationToken ct)
|
||||
{
|
||||
if (_client is null)
|
||||
{
|
||||
@@ -103,23 +104,29 @@ public sealed class TelegramAdapter : IMessangerAdapterSetup
|
||||
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 (inline is not null && inline.Any())
|
||||
if (req.Inline is not null && req.Inline.Any())
|
||||
{
|
||||
inlineMarkup = new InlineKeyboardMarkup(
|
||||
inline.Select(row => row.Select(b => new InlineKeyboardButton(b.Label, b.Value)).ToArray())
|
||||
.ToArray()
|
||||
req.Inline.Select(row => row.Select(b => new InlineKeyboardButton(b.Label, b.Value)).ToArray()).ToArray()
|
||||
);
|
||||
}
|
||||
else if (reply is not null)
|
||||
else if (req.Reply is not null)
|
||||
{
|
||||
if (reply.Any())
|
||||
if (req.Reply.Any())
|
||||
{
|
||||
markup = new ReplyKeyboardMarkup(
|
||||
reply.Select(row => row.Select(b => new KeyboardButton(b.Label)).ToArray()).ToArray()
|
||||
)
|
||||
markup = new ReplyKeyboardMarkup(req.Reply.Select(row => row.Select(b => new KeyboardButton(b.Label)).ToArray()).ToArray())
|
||||
{
|
||||
ResizeKeyboard = true
|
||||
};
|
||||
@@ -130,180 +137,169 @@ public sealed class TelegramAdapter : IMessangerAdapterSetup
|
||||
}
|
||||
}
|
||||
|
||||
var parseMode = ParseMode.None;
|
||||
|
||||
switch (format)
|
||||
// If there is a file — send file
|
||||
if (req.File is not null)
|
||||
{
|
||||
case MessageFormat.Html:
|
||||
{
|
||||
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.Plain:
|
||||
{
|
||||
parseMode = ParseMode.None;
|
||||
break;
|
||||
}
|
||||
case MessageFormat.Markdown:
|
||||
{
|
||||
case MessageFormat.Markdown:
|
||||
parseMode = ParseMode.MarkdownV2;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
_logger.Log(LogLevel.Warn, $"MessageFormat '{format}' not supported. Degraded to plain text.");
|
||||
case MessageFormat.Plain:
|
||||
case null:
|
||||
parseMode = ParseMode.None;
|
||||
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
|
||||
);
|
||||
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 message.Id.ToString();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SendFileAsync(string chatId,
|
||||
public Task<string?> SendTextAsync(string chatId, string text,
|
||||
MessageFormat format = MessageFormat.Plain,
|
||||
IEnumerable<IEnumerable<InlineButton>>? inline = null,
|
||||
IEnumerable<IEnumerable<ReplyButton>>? 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendFileAsync(string chatId,
|
||||
FileDescriptor file,
|
||||
string? caption = null,
|
||||
MessageFormat? captionFormat = null,
|
||||
IEnumerable<IEnumerable<InlineButton>>? inline = null,
|
||||
IEnumerable<IEnumerable<ReplyButton>>? reply = null,
|
||||
object? adapterOptions = null,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
if (_client is null)
|
||||
var req = new SendRequest
|
||||
{
|
||||
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized.");
|
||||
return;
|
||||
}
|
||||
ChatId = chatId,
|
||||
File = file,
|
||||
Caption = caption,
|
||||
CaptionFormat = captionFormat,
|
||||
Inline = inline,
|
||||
Reply = reply,
|
||||
AdapterOptions = adapterOptions as AdapterOptionsBag
|
||||
};
|
||||
|
||||
// Получаем поток, если он задан
|
||||
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;
|
||||
}
|
||||
return SendAsync(req, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
14
BotPages.Telegram/TelegramOptions.cs
Normal file
14
BotPages.Telegram/TelegramOptions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace BotPages.Telegram;
|
||||
|
||||
/// <summary>
|
||||
/// Настройки адаптера Telegram, применяемые по умолчанию при отправке сообщений.
|
||||
/// </summary>
|
||||
public sealed class TelegramOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Отправлять ли уведомление (sound) при отправке сообщения. По умолчанию true.
|
||||
/// </summary>
|
||||
public bool NotifyOnSend { get; set; } = true;
|
||||
|
||||
// В будущем можно добавить: default parse mode, disable web page preview и т.д.
|
||||
}
|
||||
Reference in New Issue
Block a user