Новый api отправки сообщений

This commit is contained in:
FrigaT
2025-12-24 08:43:46 +03:00
parent 833d8c80d9
commit 37cb6599ba
19 changed files with 478 additions and 338 deletions

View File

@@ -0,0 +1,44 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Êîíòåéíåð äëÿ àäàïòåð-ñïåöèôè÷íûõ îïöèé, ïîçâîëÿþùèé õðàíèòü ïàðàìåòðû äëÿ íåñêîëüêèõ àäàïòåðîâ.
/// Èñïîëüçóåòñÿ âíóòðè `SendRequest.AdapterOptions`.
/// </summary>
public sealed class AdapterOptionsBag
{
private readonly Dictionary<string, object?> _map = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Óñòàíîâèòü îïöèè äëÿ àäàïòåðà.
/// </summary>
public void Set<T>(string adapterKey, T options)
{
if (adapterKey is null) throw new ArgumentNullException(nameof(adapterKey));
_map[adapterKey] = options;
}
/// <summary>
/// Ïîïðîáîâàòü ïîëó÷èòü îïöèè äëÿ àäàïòåðà.
/// </summary>
public bool TryGet<T>(string adapterKey, out T? options)
{
if (adapterKey is null) throw new ArgumentNullException(nameof(adapterKey));
if (_map.TryGetValue(adapterKey, out var o) && o is T t)
{
options = t;
return true;
}
options = default;
return false;
}
/// <summary>
/// Ïîëó÷èòü îïöèè èëè âåðíóòü null.
/// </summary>
public T? GetOrDefault<T>(string adapterKey)
{
TryGet(adapterKey, out T? v);
return v;
}
}

View File

@@ -15,27 +15,9 @@ public interface IMessengerAdapter
Capabilities Capabilities { get; }
/// <summary>
/// Отправить текстовое сообщение в чат.
/// Универсальный метод отправки с использованием общего описания запроса.
/// </summary>
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>
/// Отправить файл в чат.
/// </summary>
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);
Task<string?> SendAsync(SendRequest request, CancellationToken ct = default);
/// <summary>
/// Создать билдер альбома для отправки медиагруппы.

View File

@@ -0,0 +1,43 @@
using BotPages.Core.Messaging;
namespace BotPages.Core.Abstractions;
/// <summary>
/// Óíèâåðñàëüíàÿ ñòðóêòóðà çàïðîñà íà îòïðàâêó ñîîáùåíèÿ/ôàéëà, èñïîëüçóåìàÿ àäàïòåðàìè.
/// Ïîìåùåíà â Core ÷òîáû áûòü äîñòóïíîé äëÿ âñåõ àäàïòåðîâ.
/// </summary>
public sealed class SendRequest
{
/// <summary>Èäåíòèôèêàòîð ÷àòà/ñåññèè â ìåññåíäæåðå.</summary>
public required string ChatId { get; init; }
/// <summary>Òåêñò ñîîáùåíèÿ (åñëè îòïðàâëÿåòñÿ òåêñò).</summary>
public string? Text { get; init; }
/// <summary>Ôîðìàò òåêñòà (HTML/Markdown/Plain).</summary>
public MessageFormat? TextFormat { get; init; }
/// <summary>Inline êíîïêè (ñòðîêè êíîïîê).</summary>
public IEnumerable<IEnumerable<InlineButton>>? Inline { get; init; }
/// <summary>Reply êëàâèàòóðà (ñòðîêè êíîïîê).</summary>
public IEnumerable<IEnumerable<ReplyButton>>? Reply { get; init; }
/// <summary>Id ðåäàêòèðóåìîãî ñîîáùåíèÿ (åñëè ðåäàêòèðóåì).</summary>
public string? MessageId { get; init; }
/// <summary>Ôàéë äëÿ îòïðàâêè (åñëè îòïðàâëÿåòñÿ ôàéë).</summary>
public FileDescriptor? File { get; init; }
/// <summary>Ïîäïèñü äëÿ ôàéëà.</summary>
public string? Caption { get; init; }
/// <summary>Ôîðìàò ïîäïèñè/ïîäïèñè äëÿ ôàéëà.</summary>
public MessageFormat? CaptionFormat { get; init; }
/// <summary>
/// Êîíòåéíåð àäàïòåð-ñïåöèôè÷íûõ îïöèé.
/// Ñîäåðæèò èìåíà/êëþ÷è àäàïòåðîâ è ñîîòâåòñòâóþùèå îáúåêòû îïöèé.
/// </summary>
public AdapterOptionsBag? AdapterOptions { get; init; }
}

View File

@@ -4,12 +4,19 @@ using BotPages.Core.Messaging;
namespace BotPages.Core;
/// <summary>
/// Расширения <see cref="PageContext"/> для работы с <see cref="IMessengerAdapter"/>
/// Расширения <see cref="PageContext"/> для работы с адаптером.
/// Упрощают создание универсального `SendRequest`.
/// </summary>
public static class PageContextAdapterExtensions
{
/// <summary>
/// Отправить текстовое сообщение.
/// Отправить универсальный запрос через привязанный адаптер.
/// </summary>
public static Task<string?> SendAsync(this PageContext ctx, SendRequest request, CancellationToken ct = default)
=> ctx.Adapter.SendAsync(request, ct);
/// <summary>
/// Удобная оболочка: отправить текстовое сообщение.
/// </summary>
public static Task<string?> SendTextAsync(this PageContext ctx,
string text,
@@ -17,25 +24,56 @@ public static class PageContextAdapterExtensions
IEnumerable<IEnumerable<InlineButton>>? inline = null,
IEnumerable<IEnumerable<ReplyButton>>? reply = null,
string? messageId = null,
object? adapterOptions = null,
CancellationToken ct = default)
=> ctx.Adapter.SendTextAsync(ctx.Update.Chat.Id, text, format, inline, reply, messageId, ct);
{
var bag = adapterOptions switch
{
null => null,
AdapterOptionsBag b => b,
_ => throw new ArgumentException("adapterOptions must be an AdapterOptionsBag or null. Use MessageBuilder extensions to set adapter options.", nameof(adapterOptions))
};
return ctx.SendAsync(new SendRequest
{
ChatId = ctx.Update.Chat.Id,
Text = text,
TextFormat = format,
Inline = inline,
Reply = reply,
MessageId = messageId,
AdapterOptions = bag
}, ct);
}
/// <summary>
/// Отправить файл.
/// Удобная оболочка: отправить файл.
/// </summary>
public static Task SendFileAsync(this PageContext ctx,
public static Task<string?> SendFileAsync(this PageContext ctx,
FileDescriptor file,
string? caption = null,
MessageFormat? captionFormat = null,
IEnumerable<IEnumerable<InlineButton>>? inline = null,
IEnumerable<IEnumerable<ReplyButton>>? reply = null,
CancellationToken ct = default
)
=> ctx.Adapter.SendFileAsync(chatId: ctx.Update.Chat.Id,
file: file,
caption: caption,
captionFormat: captionFormat,
inline: inline,
reply: reply,
ct: ct);
object? adapterOptions = null,
CancellationToken ct = default)
{
var bag = adapterOptions switch
{
null => null,
AdapterOptionsBag b => b,
_ => throw new ArgumentException("adapterOptions must be an AdapterOptionsBag or null. Use MessageBuilder extensions to set adapter options.", nameof(adapterOptions))
};
return ctx.SendAsync(new SendRequest
{
ChatId = ctx.Update.Chat.Id,
File = file,
Caption = caption,
CaptionFormat = captionFormat,
Inline = inline,
Reply = reply,
AdapterOptions = bag
}, ct);
}
}

View File

@@ -4,6 +4,7 @@ namespace BotPages.Core.Messaging;
/// <summary>
/// Fluentбилдер для отправки сообщений (текст, кнопки, файлы, альбомы, прогресс).
/// Поддерживает указание адаптер-специфичных опций через `WithAdapterOption`.
/// </summary>
public sealed class MessageBuilder
{
@@ -16,10 +17,21 @@ public sealed class MessageBuilder
private readonly List<(FileDescriptor file, string? caption, MessageFormat? captionFormat)> _album = new();
private bool _disableReplyKeyboard;
private string? _editMessageId = null;
private AdapterOptionsBag? _adapterOptions = null;
/// <summary>Создать билдер сообщений.</summary>
public MessageBuilder(PageContext ctx) => _ctx = ctx;
/// <summary>
/// Установить опции для конкретного адаптера. Ключ адаптера определяется адаптером (напр., "telegram").
/// </summary>
public MessageBuilder WithAdapterOption<T>(string adapterKey, T options)
{
if (_adapterOptions is null) _adapterOptions = new AdapterOptionsBag();
_adapterOptions.Set(adapterKey, options);
return this;
}
/// <summary>Текст сообщения.</summary>
public MessageBuilder Text(string text, MessageFormat format = MessageFormat.Plain)
{
@@ -63,7 +75,7 @@ public sealed class MessageBuilder
return this;
}
/// <summary>Добавить строку inlineкнопок.</summary>
/// <summary>Добавить строки inlineкнопок.</summary>
public MessageBuilder Inline(IEnumerable<IEnumerable<InlineButton>> row)
{
_inline.AddRange(row.Select(t => t.ToList()).ToList());
@@ -73,7 +85,6 @@ public sealed class MessageBuilder
/// <summary>
/// Отключение Reply клавиатуры.
/// </summary>
/// <returns></returns>
public MessageBuilder DisableReply()
{
_disableReplyKeyboard = true;
@@ -96,7 +107,7 @@ public sealed class MessageBuilder
return this;
}
/// <summary>Добавить строку replyкнопок.</summary>
/// <summary>Добавить строки replyкнопок.</summary>
public MessageBuilder Reply(IEnumerable<IEnumerable<ReplyButton>> row)
{
_disableReplyKeyboard = false;
@@ -130,18 +141,16 @@ public sealed class MessageBuilder
// Текст
if (!string.IsNullOrWhiteSpace(_text))
{
messageId = await _ctx.SendTextAsync(_text, _format, _inline, reply, _editMessageId, ct);
messageId = await _ctx.SendTextAsync(_text, _format, _inline, reply, _editMessageId, _adapterOptions, ct);
}
// Файлы
foreach (var (file, caption, captionFormat) in _files)
await _ctx.SendFileAsync(file: file
, caption: caption
, captionFormat: captionFormat
, reply: reply
, inline: _inline
, ct: ct
);
{
var res = await _ctx.SendFileAsync(file, caption, captionFormat, _inline, reply, _adapterOptions, ct);
// сохранить первый возвращённый id сообщения
if (messageId is null && res is not null) messageId = res;
}
// Альбом
if (_album.Count > 0)
@@ -157,6 +166,7 @@ public sealed class MessageBuilder
_album.Clear();
_editMessageId = null;
_adapterOptions = null;
return messageId;
}

View File

@@ -27,8 +27,12 @@ public sealed class ErrorHandlingMiddleware : IPageMiddleware
{
_logger.Log(LogLevel.Critical, "Unhandled exception in middleware pipeline.", ex);
// Теперь можно напрямую использовать PageContext для ответа
await ctx.SendTextAsync("Произошла ошибка при обработке запроса. Попробуйте ещё раз.", ct: ct);
// Отправляем универсальный запрос с текстом об ошибке
await ctx.SendAsync(new SendRequest
{
ChatId = ctx.Update.Chat.Id,
Text = "Произошла ошибка при обработке запроса. Попробуйте ещё раз."
}, ct);
}
}
}