Добавлены новые методы отправки сообщений

This commit is contained in:
2026-02-06 04:38:06 +03:00
parent 69ff3cf7d4
commit cd280369bc
25 changed files with 1525 additions and 219 deletions

View File

@@ -2,7 +2,7 @@ namespace BotPages.Core.Abstractions;
/// <summary>
/// Êîíòåéíåð äëÿ àäàïòåð-ñïåöèôè÷íûõ îïöèé, ïîçâîëÿþùèé õðàíèòü ïàðàìåòðû äëÿ íåñêîëüêèõ àäàïòåðîâ.
/// Èñïîëüçóåòñÿ âíóòðè `SendRequest.AdapterOptions`.
/// Èñïîëüçóåòñÿ âíóòðè <see cref="SendRequest.AdapterOptions"/>.
/// </summary>
public sealed class AdapterOptionsBag
{

View File

@@ -1,6 +1,28 @@
namespace BotPages.Core.Abstractions;
using BotPages.Core.Context;
namespace BotPages.Core.Abstractions;
/// <summary>
/// Ключ для идентификации пользовательской сессии.
/// </summary>
public readonly record struct CompositeSessionKey(string MessengerType, string ChatId, string? UserId);
public readonly record struct CompositeSessionKey(string AdapterId, string ChatId, string? UserId)
{
/// <summary>
/// Создает ключ сессии из UpdateContext.
/// </summary>
public static CompositeSessionKey FromUpdate(UpdateContext update)
{
return new CompositeSessionKey(
update.AdapterId,
update.Chat.Id,
update.User.Id);
}
/// <summary>
/// Получить ключ для определенного адаптера.
/// </summary>
public CompositeSessionKey ForAdapter(string adapterId)
{
return new CompositeSessionKey(adapterId, ChatId, UserId);
}
}

View File

@@ -1,4 +1,5 @@
using BotPages.Core.Context;
using BotPages.Core.Messaging;
namespace BotPages.Core.Abstractions;
@@ -23,6 +24,53 @@ public interface IMessengerAdapter
/// </summary>
Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default);
/// <summary>
/// Удалить несколько сообщений за раз.
/// </summary>
Task<bool> DeleteMultipleAsync(string chatId, IEnumerable<string> messageIds, CancellationToken ct = default);
/// <summary>
/// Редактировать только текст сообщения.
/// </summary>
Task<string?> EditTextAsync(string chatId, string messageId, string text,
MessageFormat? format = null, CancellationToken ct = default);
/// <summary>
/// Редактировать только клавиатуру сообщения.
/// </summary>
Task<string?> EditButtonsAsync(string chatId, string messageId,
IEnumerable<IEnumerable<InlineButton>>? inlineButtons = null,
CancellationToken ct = default);
/// <summary>
/// Закрепить сообщение в чате.
/// </summary>
Task<bool> PinMessageAsync(string chatId, string messageId, bool disableNotification = false,
CancellationToken ct = default);
/// <summary>
/// Открепить сообщение в чате.
/// </summary>
Task<bool> UnpinMessageAsync(string chatId, string messageId, CancellationToken ct = default);
/// <summary>
/// Получить информацию о сообщении.
/// </summary>
Task<MessageInfo?> GetMessageInfoAsync(string chatId, string messageId, CancellationToken ct = default);
/// <summary>
/// Переслать сообщение.
/// </summary>
Task<string?> ForwardMessageAsync(string fromChatId, string messageId, string toChatId,
bool disableNotification = false, CancellationToken ct = default);
/// <summary>
/// Копировать сообщение с возможностью редактирования.
/// </summary>
Task<string?> CopyMessageAsync(string fromChatId, string messageId, string toChatId,
string? caption = null, MessageFormat? captionFormat = null,
bool disableNotification = false, CancellationToken ct = default);
/// <summary>
/// Создать билдер альбома для отправки медиагруппы.
/// </summary>

View File

@@ -1,44 +1,53 @@
using BotPages.Core.Context;
namespace BotPages.Core.Abstractions;
namespace BotPages.Core.Abstractions;
/// <summary>
/// Фабрика адаптеров мессенджеров.
/// Используется для разрешения конкретного <see cref="IMessengerAdapterSetup"/> по типу мессенджера.
/// Используется для разрешения конкретного <see cref="IMessengerAdapterSetup"/> по ID адаптера.
/// </summary>
public interface IMessengerAdapterFactory
{
/// <summary>
/// Список зарегистрированных адаптеров.
/// Список всех зарегистрированных адаптеров.
/// </summary>
Dictionary<string, IMessengerAdapterSetup> Adapters { get; }
IReadOnlyList<IMessengerAdapterSetup> AllAdapters { get; }
/// <summary>
/// Зарегистрировать адаптер для указанного типа мессенджера.
/// Зарегистрировать адаптер с уникальным идентификатором.
/// </summary>
/// <param name="messengerType">
/// Тип мессенджера (например, "Telegram", "Slack", "VK").
/// </param>
/// <param name="adapter">
/// Экземпляр адаптера, реализующий <see cref="IMessengerAdapter"/>.
/// </param>
/// <returns>
/// Текущий экземпляр <see cref="IMessengerAdapterFactory"/> для цепочки вызовов.
/// </returns>
IMessengerAdapterFactory Register(string messengerType, IMessengerAdapterSetup adapter);
/// <param name="adapterId">Уникальный идентификатор адаптера.</param>
/// <param name="adapter">Экземпляр адаптера.</param>
/// <returns>Текущий экземпляр фабрики для цепочки вызовов.</returns>
/// <exception cref="ArgumentException">Если адаптер с таким ID уже зарегистрирован.</exception>
IMessengerAdapterFactory Register(string adapterId, IMessengerAdapterSetup adapter);
/// <summary>
/// Получить адаптер для указанного мессенджера.
/// Зарегистрировать адаптер с автоматически сгенерированным ID.
/// </summary>
/// <param name="messengerType">
/// Тип мессенджера (например, "Telegram", "Slack", "VK").
/// Значение должно совпадать с <see cref="UpdateContext.MessengerType"/>.
/// </param>
/// <returns>
/// Экземпляр <see cref="IMessengerAdapter"/>, зарегистрированный для данного типа мессенджера.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если адаптер для указанного типа не зарегистрирован.
/// </exception>
IMessengerAdapter Resolve(string messengerType);
}
IMessengerAdapterFactory Register(IMessengerAdapterSetup adapter);
/// <summary>
/// Получить адаптер по ID.
/// </summary>
/// <exception cref="InvalidOperationException">Если адаптер не найден.</exception>
IMessengerAdapter Resolve(string adapterId);
/// <summary>
/// Попытаться получить адаптер по ID.
/// </summary>
bool TryResolve(string adapterId, out IMessengerAdapter? adapter);
/// <summary>
/// Получить все адаптеры определенного типа.
/// </summary>
IReadOnlyList<IMessengerAdapter> GetAdaptersByType(string adapterType);
/// <summary>
/// Проверить, зарегистрирован ли адаптер с указанным ID.
/// </summary>
bool Contains(string adapterId);
/// <summary>
/// Удалить адаптер по ID.
/// </summary>
bool Remove(string adapterId);
}

View File

@@ -0,0 +1,34 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Информация о сообщении.
/// </summary>
public class MessageInfo
{
/// <summary>ID сообщения.</summary>
public required string MessageId { get; init; }
/// <summary>ID чата.</summary>
public required string ChatId { get; init; }
/// <summary>Текст сообщения.</summary>
public string? Text { get; init; }
/// <summary>Формат текста.</summary>
public MessageFormat? Format { get; init; }
/// <summary>Дата отправки.</summary>
public DateTime Date { get; init; }
/// <summary>ID отправителя.</summary>
public string? FromUserId { get; init; }
/// <summary>Закреплено ли сообщение.</summary>
public bool IsPinned { get; init; }
/// <summary>Является ли ответом на другое сообщение.</summary>
public bool IsReply { get; init; }
/// <summary>ID сообщения, на которое отвечает.</summary>
public string? ReplyToMessageId { get; init; }
}

View File

@@ -0,0 +1,102 @@
using BotPages.Core.Context;
using BotPages.Core.Messaging;
namespace BotPages.Core.Abstractions;
/// <summary>
/// Базовый класс для адаптеров мессенджеров.
/// </summary>
public abstract class MessengerAdapterBase : IMessengerAdapterSetup
{
/// <summary>
/// Уникальный идентификатор адаптера.
/// </summary>
public string AdapterId { get; internal set; } = string.Empty;
/// <summary>
/// Тип адаптера (Telegram, VK, WhatsApp и т.д.).
/// </summary>
public abstract string AdapterType { get; }
/// <summary>
/// Название адаптера для отображения.
/// </summary>
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// Доступные возможности мессенджера.
/// </summary>
public abstract Capabilities Capabilities { get; }
/// <summary>
/// Универсальный метод отправки с использованием общего описания запроса.
/// </summary>
public abstract Task<string?> SendAsync(SendRequest request, CancellationToken ct = default);
/// <summary>
/// Универсальный метод удаления сообщения.
/// </summary>
public abstract Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default);
/// <summary>
/// Удалить несколько сообщений за раз.
/// </summary>
public abstract Task<bool> DeleteMultipleAsync(string chatId, IEnumerable<string> messageIds, CancellationToken ct = default);
/// <summary>
/// Редактировать только текст сообщения.
/// </summary>
public abstract Task<string?> EditTextAsync(string chatId, string messageId, string text,
MessageFormat? format = null, CancellationToken ct = default);
/// <summary>
/// Редактировать только клавиатуру сообщения.
/// </summary>
public abstract Task<string?> EditButtonsAsync(string chatId, string messageId,
IEnumerable<IEnumerable<InlineButton>>? inlineButtons = null,
CancellationToken ct = default);
/// <summary>
/// Закрепить сообщение в чате.
/// </summary>
public abstract Task<bool> PinMessageAsync(string chatId, string messageId, bool disableNotification = false,
CancellationToken ct = default);
/// <summary>
/// Открепить сообщение в чате.
/// </summary>
public abstract Task<bool> UnpinMessageAsync(string chatId, string messageId, CancellationToken ct = default);
/// <summary>
/// Получить информацию о сообщении.
/// </summary>
public abstract Task<MessageInfo?> GetMessageInfoAsync(string chatId, string messageId, CancellationToken ct = default);
/// <summary>
/// Переслать сообщение.
/// </summary>
public abstract Task<string?> ForwardMessageAsync(string fromChatId, string messageId, string toChatId,
bool disableNotification = false, CancellationToken ct = default);
/// <summary>
/// Копировать сообщение с возможностью редактирования.
/// </summary>
public abstract Task<string?> CopyMessageAsync(string fromChatId, string messageId, string toChatId,
string? caption = null, MessageFormat? captionFormat = null,
bool disableNotification = false, CancellationToken ct = default);
/// <summary>
/// Создать билдер альбома для отправки медиагруппы.
/// </summary>
public abstract IAlbumBuilder CreateAlbumBuilder(PageContext ctx);
/// <summary>
/// Вызывается при выходе со страницы.
/// </summary>
public abstract Task OnLeaveAsync(PageContext ctx, CancellationToken ct);
/// <summary>
/// Запуск работы адаптера.
/// </summary>
public abstract Task StartAdapterAsync(Func<UpdateContext, Task> onUpdate, List<Routing.Command> commands, CancellationToken ct);
}

View File

@@ -1,50 +1,142 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Реализация <see cref="IMessengerAdapterFactory"/>, позволяющая регистрировать и разрешать несколько адаптеров мессенджеров.
/// Реализация <see cref="IMessengerAdapterFactory"/>, позволяющая регистрировать и разрешать несколько адаптеров.
/// </summary>
public sealed class MultiAdapterFactory : IMessengerAdapterFactory
{
private readonly Dictionary<string, IMessengerAdapterSetup> _adapters = new(StringComparer.OrdinalIgnoreCase);
private readonly List<IMessengerAdapterSetup> _allAdapters = new();
private readonly Dictionary<string, IMessengerAdapterSetup> _adaptersById = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<IMessengerAdapterSetup>> _adaptersByType = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Список зарегистрированных адаптеров.
/// Список всех зарегистрированных адаптеров.
/// </summary>
public Dictionary<string, IMessengerAdapterSetup> Adapters => _adapters;
public IReadOnlyList<IMessengerAdapterSetup> AllAdapters => _allAdapters;
/// <summary>
/// Зарегистрировать адаптер для указанного типа мессенджера.
/// Зарегистрировать адаптер с уникальным идентификатором.
/// </summary>
/// <param name="messengerType">
/// Тип мессенджера (например, "Telegram", "Slack", "VK").
/// </param>
/// <param name="adapter">
/// Экземпляр адаптера, реализующий <see cref="IMessengerAdapter"/>.
/// </param>
/// <returns>
/// Текущий экземпляр <see cref="MultiAdapterFactory"/> для цепочки вызовов.
/// </returns>
public IMessengerAdapterFactory Register(string messengerType, IMessengerAdapterSetup adapter)
public IMessengerAdapterFactory Register(string adapterId, IMessengerAdapterSetup adapter)
{
_adapters[messengerType] = adapter;
if (string.IsNullOrWhiteSpace(adapterId))
throw new ArgumentException("Adapter ID cannot be null or empty", nameof(adapterId));
if (_adaptersById.ContainsKey(adapterId))
throw new ArgumentException($"Adapter with ID '{adapterId}' is already registered", nameof(adapterId));
// Устанавливаем идентификатор в адаптер
if (adapter is MessengerAdapterBase adapterBase)
{
adapterBase.AdapterId = adapterId;
}
_allAdapters.Add(adapter);
_adaptersById[adapterId] = adapter;
// Группируем по типу
var adapterType = GetAdapterType(adapter);
if (!_adaptersByType.TryGetValue(adapterType, out var typeList))
{
typeList = new List<IMessengerAdapterSetup>();
_adaptersByType[adapterType] = typeList;
}
typeList.Add(adapter);
return this;
}
/// <summary>
/// Получить адаптер для указанного мессенджера.
/// Зарегистрировать адаптер с автоматически сгенерированным ID.
/// </summary>
/// <param name="messengerType">
/// Тип мессенджера (например, "Telegram", "Slack", "VK").
/// </param>
/// <returns>
/// Экземпляр <see cref="IMessengerAdapter"/>, зарегистрированный для данного типа мессенджера.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если адаптер для указанного типа не зарегистрирован.
/// </exception>
public IMessengerAdapter Resolve(string messengerType)
=> _adapters.TryGetValue(messengerType, out var adapter)
? adapter
: throw new InvalidOperationException($"No adapter registered for {messengerType}");
}
public IMessengerAdapterFactory Register(IMessengerAdapterSetup adapter)
{
var adapterId = GenerateAdapterId(adapter);
return Register(adapterId, adapter);
}
/// <summary>
/// Получить адаптер по ID.
/// </summary>
public IMessengerAdapter Resolve(string adapterId)
{
if (_adaptersById.TryGetValue(adapterId, out var adapter))
{
return adapter;
}
throw new InvalidOperationException($"No adapter registered with ID '{adapterId}'");
}
/// <summary>
/// Попытаться получить адаптер по ID.
/// </summary>
public bool TryResolve(string adapterId, out IMessengerAdapter? adapter)
{
var result = _adaptersById.TryGetValue(adapterId, out var adapterSetup);
adapter = adapterSetup;
return result;
}
/// <summary>
/// Получить все адаптеры определенного типа.
/// </summary>
public IReadOnlyList<IMessengerAdapter> GetAdaptersByType(string adapterType)
{
if (_adaptersByType.TryGetValue(adapterType, out var adapters))
{
return adapters.AsReadOnly();
}
return Array.Empty<IMessengerAdapter>();
}
/// <summary>
/// Проверить, зарегистрирован ли адаптер с указанным ID.
/// </summary>
public bool Contains(string adapterId) => _adaptersById.ContainsKey(adapterId);
/// <summary>
/// Удалить адаптер по ID.
/// </summary>
public bool Remove(string adapterId)
{
if (_adaptersById.TryGetValue(adapterId, out var adapter))
{
_allAdapters.Remove(adapter);
_adaptersById.Remove(adapterId);
var adapterType = GetAdapterType(adapter);
if (_adaptersByType.TryGetValue(adapterType, out var typeList))
{
typeList.Remove(adapter);
if (typeList.Count == 0)
{
_adaptersByType.Remove(adapterType);
}
}
return true;
}
return false;
}
private static string GetAdapterType(IMessengerAdapter adapter)
{
if (adapter is MessengerAdapterBase adapterBase)
{
return adapterBase.AdapterType;
}
// Для обратной совместимости
return adapter.GetType().Name.Replace("Adapter", "");
}
private static string GenerateAdapterId(IMessengerAdapter adapter)
{
var adapterType = GetAdapterType(adapter);
var guid = Guid.NewGuid().ToString("N").Substring(0, 8);
return $"{adapterType}_{guid}".ToLowerInvariant();
}
}

View File

@@ -39,4 +39,66 @@ public sealed class SendRequest
/// Содержит имена/ключи адаптеров и соответствующие объекты опций.
/// </summary>
public AdapterOptionsBag? AdapterOptions { get; init; }
/// <summary>
/// ID сообщения, на которое отвечаем.
/// </summary>
public string? ReplyToMessageId { get; init; }
/// <summary>
/// Цитировать ли оригинальное сообщение при ответе.
/// </summary>
public bool QuoteReply { get; init; } = true;
/// <summary>
/// Заголовок цитаты (для некоторых мессенджеров).
/// </summary>
public string? QuoteTitle { get; init; }
/// <summary>
/// Показывать ли предпросмотр ссылок в сообщении.
/// </summary>
public bool DisableWebPagePreview { get; init; } = false;
/// <summary>
/// Отключает уведомление о сообщении.
/// </summary>
public bool DisableNotification { get; init; } = false;
/// <summary>
/// Защищает содержимое сообщения от пересылки и сохранения.
/// </summary>
public bool ProtectContent { get; init; } = false;
/// <summary>
/// Стиль разметки сообщения (для некоторых мессенджеров).
/// </summary>
public MessageStyle? Style { get; init; }
/// <summary>
/// Позволяет указать дату отправки сообщения (для планирования).
/// </summary>
public DateTime? ScheduleDate { get; init; }
/// <summary>
/// Тема сообщения (для форумов и тредов).
/// </summary>
public string? Topic { get; init; }
}
/// <summary>
/// Стиль оформления сообщения.
/// </summary>
public enum MessageStyle
{
/// <summary>Обычный стиль.</summary>
Default,
/// <summary>Стиль заголовка.</summary>
Heading,
/// <summary>Стиль предупреждения.</summary>
Warning,
/// <summary>Стиль успеха.</summary>
Success,
/// <summary>Стиль ошибки.</summary>
Error
}