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

This commit is contained in:
2026-02-06 07:45:19 +03:00
parent cd280369bc
commit 5dc071c750
10 changed files with 79 additions and 165 deletions

View File

@@ -13,7 +13,7 @@ public readonly record struct CompositeSessionKey(string AdapterId, string ChatI
public static CompositeSessionKey FromUpdate(UpdateContext update) public static CompositeSessionKey FromUpdate(UpdateContext update)
{ {
return new CompositeSessionKey( return new CompositeSessionKey(
update.AdapterId, update.Adapter.Id,
update.Chat.Id, update.Chat.Id,
update.User.Id); update.User.Id);
} }

View File

@@ -9,6 +9,16 @@ namespace BotPages.Core.Abstractions;
/// </summary> /// </summary>
public interface IMessengerAdapter public interface IMessengerAdapter
{ {
/// <summary>
/// Уникальный идентификатор адаптера.
/// </summary>
string Id { get; }
/// <summary>
/// Тип адаптера (Telegram, VK, WhatsApp и т.д.).
/// </summary>
string Type { get; }
/// <summary> /// <summary>
/// Доступные возможности мессенджера. /// Доступные возможности мессенджера.
/// </summary> /// </summary>
@@ -87,10 +97,16 @@ public interface IMessengerAdapter
/// </summary> /// </summary>
public interface IMessengerAdapterSetup : IMessengerAdapter public interface IMessengerAdapterSetup : IMessengerAdapter
{ {
/// <summary>
/// Внутренний метод для установки ID адаптера.
/// </summary>
void SetAdapterId(string adapterId);
/// <summary> /// <summary>
/// Запуск работы адаптера /// Запуск работы адаптера
/// </summary> /// </summary>
/// <param name="onUpdate"></param> /// <param name="onUpdate"></param>
/// <param name="commands"></param>
/// <param name="ct"></param> /// <param name="ct"></param>
/// <returns></returns> /// <returns></returns>
Task StartAdapterAsync(Func<UpdateContext, Task> onUpdate, List<Routing.Command> commands, CancellationToken ct); Task StartAdapterAsync(Func<UpdateContext, Task> onUpdate, List<Routing.Command> commands, CancellationToken ct);

View File

@@ -1,102 +0,0 @@
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

@@ -26,16 +26,13 @@ public sealed class MultiAdapterFactory : IMessengerAdapterFactory
throw new ArgumentException($"Adapter with ID '{adapterId}' is already registered", nameof(adapterId)); throw new ArgumentException($"Adapter with ID '{adapterId}' is already registered", nameof(adapterId));
// Устанавливаем идентификатор в адаптер // Устанавливаем идентификатор в адаптер
if (adapter is MessengerAdapterBase adapterBase) adapter.SetAdapterId(adapterId);
{
adapterBase.AdapterId = adapterId;
}
_allAdapters.Add(adapter); _allAdapters.Add(adapter);
_adaptersById[adapterId] = adapter; _adaptersById[adapterId] = adapter;
// Группируем по типу // Группируем по типу
var adapterType = GetAdapterType(adapter); var adapterType = adapter.Type;
if (!_adaptersByType.TryGetValue(adapterType, out var typeList)) if (!_adaptersByType.TryGetValue(adapterType, out var typeList))
{ {
typeList = new List<IMessengerAdapterSetup>(); typeList = new List<IMessengerAdapterSetup>();
@@ -106,7 +103,7 @@ public sealed class MultiAdapterFactory : IMessengerAdapterFactory
_allAdapters.Remove(adapter); _allAdapters.Remove(adapter);
_adaptersById.Remove(adapterId); _adaptersById.Remove(adapterId);
var adapterType = GetAdapterType(adapter); var adapterType = adapter.Type;
if (_adaptersByType.TryGetValue(adapterType, out var typeList)) if (_adaptersByType.TryGetValue(adapterType, out var typeList))
{ {
typeList.Remove(adapter); typeList.Remove(adapter);
@@ -122,20 +119,9 @@ public sealed class MultiAdapterFactory : IMessengerAdapterFactory
return false; 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) private static string GenerateAdapterId(IMessengerAdapter adapter)
{ {
var adapterType = GetAdapterType(adapter); var adapterType = adapter.Type;
var guid = Guid.NewGuid().ToString("N").Substring(0, 8); var guid = Guid.NewGuid().ToString("N").Substring(0, 8);
return $"{adapterType}_{guid}".ToLowerInvariant(); return $"{adapterType}_{guid}".ToLowerInvariant();
} }

View File

@@ -215,7 +215,7 @@ public sealed class BotPagesApp
SessionKey = sessionKey, SessionKey = sessionKey,
StateStorage = _state, StateStorage = _state,
Navigation = _navigation, Navigation = _navigation,
Adapter = _adapterFactory.Resolve(update.AdapterId), Adapter = _adapterFactory.Resolve(update.Adapter.Id),
AdapterFactory = _adapterFactory, AdapterFactory = _adapterFactory,
}; };

View File

@@ -52,12 +52,12 @@ public sealed class PageContext
/// <summary> /// <summary>
/// Получить текущий тип адаптера. /// Получить текущий тип адаптера.
/// </summary> /// </summary>
public string CurrentAdapterType => Update.AdapterType; public string CurrentAdapterType => Update.Adapter.Type;
/// <summary> /// <summary>
/// Получить текущий ID адаптера. /// Получить текущий ID адаптера.
/// </summary> /// </summary>
public string CurrentAdapterId => Update.AdapterId; public string CurrentAdapterId => Update.Adapter.Id;
/// <summary> /// <summary>
/// Получить все адаптеры. /// Получить все адаптеры.

View File

@@ -8,11 +8,8 @@ namespace BotPages.Core.Context;
/// </summary> /// </summary>
public sealed class UpdateContext public sealed class UpdateContext
{ {
/// <summary>Идентификатор адаптера, от которого пришло обновление.</summary> /// <summary>Адаптер, от которого пришло обновление.</summary>
public required string AdapterId { get; init; } public required IMessengerAdapter Adapter { get; init; }
/// <summary>Тип адаптера (определяется адаптером).</summary>
public required string AdapterType { get; init; }
/// <summary> /// <summary>
/// Данные пользователя, от которого пришло обновление. /// Данные пользователя, от которого пришло обновление.

View File

@@ -17,7 +17,7 @@ public sealed class LoggingMiddleware : IPageMiddleware
public async Task InvokeAsync(PageContext ctx, Func<Task> next, CancellationToken ct) public async Task InvokeAsync(PageContext ctx, Func<Task> next, CancellationToken ct)
{ {
// Логируем базовую информацию // Логируем базовую информацию
_logger.Log(LogLevel.Info, $"Update from {ctx.Update.AdapterId} | Chat: {ctx.Update.Chat.Id} | User: {ctx.Update.User.Id}"); _logger.Log(LogLevel.Info, $"Update from {ctx.Update.Adapter.Id} | Chat: {ctx.Update.Chat.Id} | User: {ctx.Update.User.Id}");
// Логируем текст, кнопки, файлы // Логируем текст, кнопки, файлы
if (ctx.Update.Text is not null) if (ctx.Update.Text is not null)

View File

@@ -20,7 +20,7 @@ namespace BotPages.Telegram;
/// Адаптер для Telegram на базе Telegram.Bot. /// Адаптер для Telegram на базе Telegram.Bot.
/// Реализует отправку текста, кнопок, файлов, альбомов и прогресса. /// Реализует отправку текста, кнопок, файлов, альбомов и прогресса.
/// </summary> /// </summary>
public sealed class TelegramAdapter : MessengerAdapterBase public sealed class TelegramAdapter : IMessengerAdapterSetup
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private TelegramBotClient? _client; private TelegramBotClient? _client;
@@ -43,27 +43,33 @@ public sealed class TelegramAdapter : MessengerAdapterBase
_token = token; _token = token;
_options = options ?? new TelegramOptions(); _options = options ?? new TelegramOptions();
// Устанавливаем имя для отображения Id = Guid.NewGuid().ToString();
DisplayName = $"Telegram Bot"; DisplayName = $"Telegram {Id}";
} }
/// <summary>Тип адаптера.</summary> /// <inheritdoc/>
public override string AdapterType => "Telegram"; public string Type => "Telegram";
/// <inheritdoc/>
public string Id { get; set; }
/// <summary> /// <summary>
/// Идентификатор мессенджера / адаптера /// Получение отображаемого имени бота (например, "Telegram: @mybot"). Доступно после запуска адаптера.
/// </summary> /// </summary>
public string MessengerType => "Telegram"; public string DisplayName { get; private set; }
/// <inheritdoc/>
public void SetAdapterId(string adapterId) => Id = adapterId;
/// <summary> /// <summary>
/// Доступные возможности адаптера. /// Доступные возможности адаптера.
/// </summary> /// </summary>
public override Capabilities Capabilities => _capabilities; public Capabilities Capabilities => _capabilities;
/// <summary> /// <summary>
/// Запустить polling для приема обновлений от Telegram. /// Запустить polling для приема обновлений от Telegram.
/// </summary> /// </summary>
public override async Task StartAdapterAsync(Func<UpdateContext, Task> onUpdate, List<BotPages.Core.Routing.Command> commands, CancellationToken ct) public async Task StartAdapterAsync(Func<UpdateContext, Task> onUpdate, List<BotPages.Core.Routing.Command> commands, CancellationToken ct)
{ {
_client = new TelegramBotClient(_token); _client = new TelegramBotClient(_token);
@@ -81,7 +87,7 @@ public sealed class TelegramAdapter : MessengerAdapterBase
errorHandler: async (_, ex, ct2) => errorHandler: async (_, ex, ct2) =>
{ {
_logger.Log(LogLevel.Warn, $"{AdapterType} ({AdapterId}) error.", ex); _logger.Log(LogLevel.Warn, $"{Type} ({Id}) error.", ex);
await Task.CompletedTask; await Task.CompletedTask;
}, },
@@ -92,7 +98,7 @@ public sealed class TelegramAdapter : MessengerAdapterBase
var me = await _client.GetMe(); var me = await _client.GetMe();
DisplayName = $"Telegram: @{me.Username}"; DisplayName = $"Telegram: @{me.Username}";
_logger.Log(LogLevel.Info, $"{AdapterType} ({AdapterId}) started: {DisplayName}"); _logger.Log(LogLevel.Info, $"{Type} ({Id}) started: {DisplayName}");
return; return;
} }
@@ -101,17 +107,17 @@ public sealed class TelegramAdapter : MessengerAdapterBase
/// Универсальный внутренний метод отправки — определяет, нужно ли отправлять текст или файл по параметрам. /// Универсальный внутренний метод отправки — определяет, нужно ли отправлять текст или файл по параметрам.
/// Возвращает id сообщения (или null). /// Возвращает id сообщения (или null).
/// </summary> /// </summary>
public override async Task<string?> SendAsync(SendRequest req, CancellationToken ct) public async Task<string?> SendAsync(SendRequest req, CancellationToken ct)
{ {
if (_client is null) if (_client is null)
{ {
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); _logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return null; return null;
} }
// Определите параметры адаптера (TelegramOptions) из папки или используйте параметры адаптера по умолчанию. // Определите параметры адаптера (TelegramOptions) из папки или используйте параметры адаптера по умолчанию.
TelegramOptions telegramOptions = _options; TelegramOptions telegramOptions = _options;
if (req.AdapterOptions is AdapterOptionsBag bag && bag.TryGet(AdapterType, out TelegramOptions? opt) && opt is not null) if (req.AdapterOptions is AdapterOptionsBag bag && bag.TryGet(Type, out TelegramOptions? opt) && opt is not null)
{ {
telegramOptions = opt; telegramOptions = opt;
} }
@@ -150,11 +156,11 @@ public sealed class TelegramAdapter : MessengerAdapterBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default) public Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default)
{ {
if (_client is null) if (_client is null)
{ {
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); _logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -162,11 +168,11 @@ public sealed class TelegramAdapter : MessengerAdapterBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<bool> DeleteMultipleAsync(string chatId, IEnumerable<string> messageIds, CancellationToken ct = default) public async Task<bool> DeleteMultipleAsync(string chatId, IEnumerable<string> messageIds, CancellationToken ct = default)
{ {
if (_client is null) if (_client is null)
{ {
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); _logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return false; return false;
} }
@@ -187,12 +193,12 @@ public sealed class TelegramAdapter : MessengerAdapterBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<string?> EditTextAsync(string chatId, string messageId, string text, public async Task<string?> EditTextAsync(string chatId, string messageId, string text,
MessageFormat? format = null, CancellationToken ct = default) MessageFormat? format = null, CancellationToken ct = default)
{ {
if (_client is null) if (_client is null)
{ {
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); _logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return null; return null;
} }
@@ -216,12 +222,12 @@ public sealed class TelegramAdapter : MessengerAdapterBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<string?> EditButtonsAsync(string chatId, string messageId, public async Task<string?> EditButtonsAsync(string chatId, string messageId,
IEnumerable<IEnumerable<InlineButton>>? inlineButtons = null, CancellationToken ct = default) IEnumerable<IEnumerable<InlineButton>>? inlineButtons = null, CancellationToken ct = default)
{ {
if (_client is null) if (_client is null)
{ {
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); _logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return null; return null;
} }
@@ -244,12 +250,12 @@ public sealed class TelegramAdapter : MessengerAdapterBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<bool> PinMessageAsync(string chatId, string messageId, bool disableNotification = false, public async Task<bool> PinMessageAsync(string chatId, string messageId, bool disableNotification = false,
CancellationToken ct = default) CancellationToken ct = default)
{ {
if (_client is null) if (_client is null)
{ {
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); _logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return false; return false;
} }
@@ -271,11 +277,11 @@ public sealed class TelegramAdapter : MessengerAdapterBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<bool> UnpinMessageAsync(string chatId, string messageId, CancellationToken ct = default) public async Task<bool> UnpinMessageAsync(string chatId, string messageId, CancellationToken ct = default)
{ {
if (_client is null) if (_client is null)
{ {
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); _logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return false; return false;
} }
@@ -296,18 +302,18 @@ public sealed class TelegramAdapter : MessengerAdapterBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<MessageInfo?> GetMessageInfoAsync(string chatId, string messageId, CancellationToken ct = default) public async Task<MessageInfo?> GetMessageInfoAsync(string chatId, string messageId, CancellationToken ct = default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<string?> ForwardMessageAsync(string fromChatId, string messageId, string toChatId, public async Task<string?> ForwardMessageAsync(string fromChatId, string messageId, string toChatId,
bool disableNotification = false, CancellationToken ct = default) bool disableNotification = false, CancellationToken ct = default)
{ {
if (_client is null) if (_client is null)
{ {
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); _logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return null; return null;
} }
@@ -331,13 +337,13 @@ public sealed class TelegramAdapter : MessengerAdapterBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<string?> CopyMessageAsync(string fromChatId, string messageId, string toChatId, public async Task<string?> CopyMessageAsync(string fromChatId, string messageId, string toChatId,
string? caption = null, MessageFormat? captionFormat = null, string? caption = null, MessageFormat? captionFormat = null,
bool disableNotification = false, CancellationToken ct = default) bool disableNotification = false, CancellationToken ct = default)
{ {
if (_client is null) if (_client is null)
{ {
_logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized."); _logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return null; return null;
} }
@@ -427,6 +433,12 @@ public sealed class TelegramAdapter : MessengerAdapterBase
bool disableNotification, string? caption, MessageFormat? captionFormat, int? replyToMessageId, bool disableNotification, string? caption, MessageFormat? captionFormat, int? replyToMessageId,
bool protectContent, CancellationToken ct) bool protectContent, CancellationToken ct)
{ {
if (_client is null)
{
_logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return null;
}
var inputFile = await CreateInputFileAsync(file, ct); var inputFile = await CreateInputFileAsync(file, ct);
var parseMode = GetParseMode(captionFormat); var parseMode = GetParseMode(captionFormat);
@@ -482,6 +494,12 @@ public sealed class TelegramAdapter : MessengerAdapterBase
ReplyMarkup? markup, bool disableNotification, int? replyToMessageId, ReplyMarkup? markup, bool disableNotification, int? replyToMessageId,
bool disableWebPagePreview, bool protectContent, CancellationToken ct) bool disableWebPagePreview, bool protectContent, CancellationToken ct)
{ {
if (_client is null)
{
_logger.Log(LogLevel.Critical, $"{Type} client is not initialized.");
return null;
}
var format = req.TextFormat ?? MessageFormat.Plain; var format = req.TextFormat ?? MessageFormat.Plain;
var parseMode = GetParseMode(format); var parseMode = GetParseMode(format);
@@ -533,8 +551,8 @@ public sealed class TelegramAdapter : MessengerAdapterBase
} }
/// <inheritdoc /> /// <inheritdoc />
public override IAlbumBuilder CreateAlbumBuilder(PageContext ctx) => new TelegramAlbumBuilder(this, ctx, _logger, _client); public IAlbumBuilder CreateAlbumBuilder(PageContext ctx) => new TelegramAlbumBuilder(this, ctx, _logger, _client);
/// <inheritdoc /> /// <inheritdoc />
public override Task OnLeaveAsync(PageContext ctx, CancellationToken ct) => Task.CompletedTask; public Task OnLeaveAsync(PageContext ctx, CancellationToken ct) => Task.CompletedTask;
} }

View File

@@ -182,8 +182,7 @@ public static class TelegramUpdateMapper
return new UpdateContext return new UpdateContext
{ {
AdapterId = adapter.AdapterId, Adapter = adapter,
AdapterType = adapter.AdapterType,
User = userContext, User = userContext,
Chat = chatContext, Chat = chatContext,
Text = text, Text = text,