Новый api отправки сообщений
This commit is contained in:
44
BotPages.Core/Abstractions/AdapterOptionsBag.cs
Normal file
44
BotPages.Core/Abstractions/AdapterOptionsBag.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// Создать билдер альбома для отправки медиагруппы.
|
||||
|
||||
43
BotPages.Core/Abstractions/SendRequest.cs
Normal file
43
BotPages.Core/Abstractions/SendRequest.cs
Normal 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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user