Переработанная версия ядра
All checks were successful
CI / build-test (push) Successful in 42s

This commit is contained in:
2025-12-05 12:57:05 +03:00
parent ee175a35a0
commit d817417a69
81 changed files with 2335 additions and 1453 deletions

View File

@@ -0,0 +1,25 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Возможности мессенджера (inline кнопки, альбомы, форматирование и т.д.).
/// </summary>
public sealed class Capabilities
{
/// <summary>Поддержка inline-кнопок.</summary>
public bool SupportsInlineButtons { get; init; }
/// <summary>Поддержка reply-кнопок.</summary>
public bool SupportsReplyButtons { get; init; }
/// <summary>Поддержка альбомов (медиагрупп).</summary>
public bool SupportsAlbums { get; init; }
/// <summary>Поддержка форматирования Markdown.</summary>
public bool SupportsFormattingMarkdown { get; init; }
/// <summary>Поддержка форматирования HTML.</summary>
public bool SupportsFormattingHtml { get; init; }
/// <summary>Максимальная длина сообщения.</summary>
public int MaxMessageLength { get; init; } = 4096;
}

View File

@@ -0,0 +1,6 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Ключ для идентификации пользовательской сессии.
/// </summary>
public readonly record struct CompositeSessionKey(string MessengerType, string ChatId, string? UserId);

View File

@@ -0,0 +1,22 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Описание файла, полученного или отправляемого через мессенджер.
/// </summary>
public sealed class FileDescriptor
{
/// <summary>Идентификатор файла в мессенджере.</summary>
public required string Id { get; init; }
/// <summary>Имя файла.</summary>
public required string Name { get; init; }
/// <summary>Расширение файла.</summary>
public required string Extension { get; init; }
/// <summary>Размер файла в байтах.</summary>
public long Size { get; init; }
/// <summary>MIME-тип файла.</summary>
public string? Mime { get; init; }
/// <summary>Тип файла.</summary>
public FileKind Kind { get; init; } = FileKind.Document;
/// <summary>Функция получения потока файла.</summary>
public Func<CancellationToken, Task<Stream>> GetStreamAsync { get; init; } = _ => Task.FromResult<Stream>(Stream.Null);
}

View File

@@ -0,0 +1,20 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Тип файла.
/// </summary>
public enum FileKind
{
/// <summary> Фото </summary>
Photo,
/// <summary> Документ </summary>
Document,
/// <summary> Аудио </summary>
Audio,
/// <summary> Видео </summary>
Video,
/// <summary> Стикер </summary>
Sticker,
/// <summary> Остальное </summary>
Other,
}

View File

@@ -0,0 +1,12 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Билдер отправки медиагруппы (альбома).
/// </summary>
public interface IAlbumBuilder
{
/// <summary>Добавить элемент в альбом.</summary>
IAlbumBuilder Add(FileDescriptor file, string? caption = null);
/// <summary>Отправить альбом.</summary>
Task SendAsync(CancellationToken ct = default);
}

View File

@@ -0,0 +1,42 @@
using BotPages.Core.Messaging;
namespace BotPages.Core.Abstractions;
/// <summary>
/// Контракт адаптера мессенджера.
/// Определяет операции отправки сообщений, файлов и прогресса.
/// </summary>
public interface IMessengerAdapter
{
/// <summary>
/// Отправить текстовое сообщение в чат.
/// </summary>
Task SendTextAsync(PageContext ctx, string text, MessageFormat format,
IEnumerable<IEnumerable<InlineButton>>? inline,
IEnumerable<IEnumerable<ReplyButton>>? reply, CancellationToken ct);
/// <summary>
/// Отправить файл в чат.
/// </summary>
Task SendFileAsync(PageContext ctx, FileDescriptor file, string? caption, CancellationToken ct);
/// <summary>
/// Создать билдер альбома для отправки медиагруппы.
/// </summary>
IAlbumBuilder CreateAlbumBuilder(PageContext ctx);
/// <summary>
/// Начать отображение прогресса операции.
/// </summary>
Task<string?> StartProgressAsync(PageContext ctx, string title, CancellationToken ct);
/// <summary>
/// Обновить прогресс операции.
/// </summary>
Task UpdateProgressAsync(PageContext ctx, string messageId, string title, int percent, CancellationToken ct);
/// <summary>
/// Вызывается при выходе со страницы.
/// </summary>
Task OnLeaveAsync(PageContext ctx, CancellationToken ct);
}

View File

@@ -0,0 +1,25 @@
using BotPages.Core.Context;
namespace BotPages.Core.Abstractions;
/// <summary>
/// Фабрика адаптеров мессенджеров.
/// Используется для разрешения конкретного <see cref="IMessengerAdapter"/> по типу мессенджера.
/// </summary>
public interface IMessengerAdapterFactory
{
/// <summary>
/// Получить адаптер для указанного мессенджера.
/// </summary>
/// <param name="messengerType">
/// Тип мессенджера (например, "Telegram", "Slack", "VK").
/// Значение должно совпадать с <see cref="UpdateContext.User"/>.<see cref="UserContext.MessengerType"/>.
/// </param>
/// <returns>
/// Экземпляр <see cref="IMessengerAdapter"/>, зарегистрированный для данного типа мессенджера.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если адаптер для указанного типа не зарегистрирован.
/// </exception>
IMessengerAdapter Resolve(string messengerType);
}

View File

@@ -0,0 +1,11 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Интерфейс middleware для обработки входящих обновлений.
/// </summary>
public interface IPageMiddleware
{
/// <summary>
/// Выполнить промежуточную логику, затем вызвать следующий обработчик.
/// </summary>
Task InvokeAsync(PageContext ctx, Func<Task> next, CancellationToken ct);
}

View File

@@ -0,0 +1,20 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Интерфейс универсального хранилища состояния.
/// Позволяет сохранять и восстанавливать данные между обновлениями.
/// </summary>
public interface IStateStorage
{
/// <summary>Получить состояние по ключу.</summary>
Task<T?> GetAsync<T>(CompositeSessionKey session, string key, CancellationToken ct);
/// <summary>Сохранить состояние по ключу.</summary>
Task SetAsync<T>(CompositeSessionKey session, string key, T state, CancellationToken ct);
/// <summary>Удалить состояние по ключу.</summary>
Task<bool> RemoveAsync(CompositeSessionKey session, string key, CancellationToken ct);
/// <summary>Удалить все состояния по ключу.</summary>
Task<bool> ClearAsync(CompositeSessionKey session, CancellationToken ct);
}

View File

@@ -0,0 +1,14 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Форматирование текста сообщения.
/// </summary>
public enum MessageFormat
{
/// <summary>Обычный текст без форматирования.</summary>
Plain,
/// <summary>Markdown форматирование.</summary>
Markdown,
/// <summary>HTML форматирование.</summary>
Html
}

View File

@@ -0,0 +1,45 @@
namespace BotPages.Core.Abstractions;
/// <summary>
/// Реализация <see cref="IMessengerAdapterFactory"/>, позволяющая регистрировать и разрешать несколько адаптеров мессенджеров.
/// </summary>
public sealed class MultiAdapterFactory : IMessengerAdapterFactory
{
private readonly Dictionary<string, IMessengerAdapter> _adapters = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Зарегистрировать адаптер для указанного типа мессенджера.
/// </summary>
/// <param name="messengerType">
/// Тип мессенджера (например, "Telegram", "Slack", "VK").
/// </param>
/// <param name="adapter">
/// Экземпляр адаптера, реализующий <see cref="IMessengerAdapter"/>.
/// </param>
/// <returns>
/// Текущий экземпляр <see cref="MultiAdapterFactory"/> для цепочки вызовов.
/// </returns>
public MultiAdapterFactory Register(string messengerType, IMessengerAdapter adapter)
{
_adapters[messengerType] = adapter;
return this;
}
/// <summary>
/// Получить адаптер для указанного мессенджера.
/// </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}");
}