diff --git a/BotPages.Core/Abstractions/AdapterOptionsBag.cs b/BotPages.Core/Abstractions/AdapterOptionsBag.cs
index 33a56bd..c1b7fa8 100644
--- a/BotPages.Core/Abstractions/AdapterOptionsBag.cs
+++ b/BotPages.Core/Abstractions/AdapterOptionsBag.cs
@@ -2,7 +2,7 @@ namespace BotPages.Core.Abstractions;
///
/// - , .
-/// `SendRequest.AdapterOptions`.
+/// .
///
public sealed class AdapterOptionsBag
{
diff --git a/BotPages.Core/Abstractions/CompositeSessionKey.cs b/BotPages.Core/Abstractions/CompositeSessionKey.cs
index 0190701..9809b31 100644
--- a/BotPages.Core/Abstractions/CompositeSessionKey.cs
+++ b/BotPages.Core/Abstractions/CompositeSessionKey.cs
@@ -1,6 +1,28 @@
-namespace BotPages.Core.Abstractions;
+using BotPages.Core.Context;
+
+namespace BotPages.Core.Abstractions;
///
/// Ключ для идентификации пользовательской сессии.
///
-public readonly record struct CompositeSessionKey(string MessengerType, string ChatId, string? UserId);
+public readonly record struct CompositeSessionKey(string AdapterId, string ChatId, string? UserId)
+{
+ ///
+ /// Создает ключ сессии из UpdateContext.
+ ///
+ public static CompositeSessionKey FromUpdate(UpdateContext update)
+ {
+ return new CompositeSessionKey(
+ update.AdapterId,
+ update.Chat.Id,
+ update.User.Id);
+ }
+
+ ///
+ /// Получить ключ для определенного адаптера.
+ ///
+ public CompositeSessionKey ForAdapter(string adapterId)
+ {
+ return new CompositeSessionKey(adapterId, ChatId, UserId);
+ }
+}
\ No newline at end of file
diff --git a/BotPages.Core/Abstractions/IMessengerAdapter.cs b/BotPages.Core/Abstractions/IMessengerAdapter.cs
index 1fb9b27..e92f31b 100644
--- a/BotPages.Core/Abstractions/IMessengerAdapter.cs
+++ b/BotPages.Core/Abstractions/IMessengerAdapter.cs
@@ -1,4 +1,5 @@
using BotPages.Core.Context;
+using BotPages.Core.Messaging;
namespace BotPages.Core.Abstractions;
@@ -23,6 +24,53 @@ public interface IMessengerAdapter
///
Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default);
+ ///
+ /// Удалить несколько сообщений за раз.
+ ///
+ Task DeleteMultipleAsync(string chatId, IEnumerable messageIds, CancellationToken ct = default);
+
+ ///
+ /// Редактировать только текст сообщения.
+ ///
+ Task EditTextAsync(string chatId, string messageId, string text,
+ MessageFormat? format = null, CancellationToken ct = default);
+
+ ///
+ /// Редактировать только клавиатуру сообщения.
+ ///
+ Task EditButtonsAsync(string chatId, string messageId,
+ IEnumerable>? inlineButtons = null,
+ CancellationToken ct = default);
+
+ ///
+ /// Закрепить сообщение в чате.
+ ///
+ Task PinMessageAsync(string chatId, string messageId, bool disableNotification = false,
+ CancellationToken ct = default);
+
+ ///
+ /// Открепить сообщение в чате.
+ ///
+ Task UnpinMessageAsync(string chatId, string messageId, CancellationToken ct = default);
+
+ ///
+ /// Получить информацию о сообщении.
+ ///
+ Task GetMessageInfoAsync(string chatId, string messageId, CancellationToken ct = default);
+
+ ///
+ /// Переслать сообщение.
+ ///
+ Task ForwardMessageAsync(string fromChatId, string messageId, string toChatId,
+ bool disableNotification = false, CancellationToken ct = default);
+
+ ///
+ /// Копировать сообщение с возможностью редактирования.
+ ///
+ Task CopyMessageAsync(string fromChatId, string messageId, string toChatId,
+ string? caption = null, MessageFormat? captionFormat = null,
+ bool disableNotification = false, CancellationToken ct = default);
+
///
/// Создать билдер альбома для отправки медиагруппы.
///
diff --git a/BotPages.Core/Abstractions/IMessengerAdapterFactory.cs b/BotPages.Core/Abstractions/IMessengerAdapterFactory.cs
index 2235438..2ecfe6b 100644
--- a/BotPages.Core/Abstractions/IMessengerAdapterFactory.cs
+++ b/BotPages.Core/Abstractions/IMessengerAdapterFactory.cs
@@ -1,44 +1,53 @@
-using BotPages.Core.Context;
-
-namespace BotPages.Core.Abstractions;
+namespace BotPages.Core.Abstractions;
///
/// Фабрика адаптеров мессенджеров.
-/// Используется для разрешения конкретного по типу мессенджера.
+/// Используется для разрешения конкретного по ID адаптера.
///
public interface IMessengerAdapterFactory
{
///
- /// Список зарегистрированных адаптеров.
+ /// Список всех зарегистрированных адаптеров.
///
- Dictionary Adapters { get; }
+ IReadOnlyList AllAdapters { get; }
///
- /// Зарегистрировать адаптер для указанного типа мессенджера.
+ /// Зарегистрировать адаптер с уникальным идентификатором.
///
- ///
- /// Тип мессенджера (например, "Telegram", "Slack", "VK").
- ///
- ///
- /// Экземпляр адаптера, реализующий .
- ///
- ///
- /// Текущий экземпляр для цепочки вызовов.
- ///
- IMessengerAdapterFactory Register(string messengerType, IMessengerAdapterSetup adapter);
+ /// Уникальный идентификатор адаптера.
+ /// Экземпляр адаптера.
+ /// Текущий экземпляр фабрики для цепочки вызовов.
+ /// Если адаптер с таким ID уже зарегистрирован.
+ IMessengerAdapterFactory Register(string adapterId, IMessengerAdapterSetup adapter);
///
- /// Получить адаптер для указанного мессенджера.
+ /// Зарегистрировать адаптер с автоматически сгенерированным ID.
///
- ///
- /// Тип мессенджера (например, "Telegram", "Slack", "VK").
- /// Значение должно совпадать с .
- ///
- ///
- /// Экземпляр , зарегистрированный для данного типа мессенджера.
- ///
- ///
- /// Выбрасывается, если адаптер для указанного типа не зарегистрирован.
- ///
- IMessengerAdapter Resolve(string messengerType);
-}
+ IMessengerAdapterFactory Register(IMessengerAdapterSetup adapter);
+
+ ///
+ /// Получить адаптер по ID.
+ ///
+ /// Если адаптер не найден.
+ IMessengerAdapter Resolve(string adapterId);
+
+ ///
+ /// Попытаться получить адаптер по ID.
+ ///
+ bool TryResolve(string adapterId, out IMessengerAdapter? adapter);
+
+ ///
+ /// Получить все адаптеры определенного типа.
+ ///
+ IReadOnlyList GetAdaptersByType(string adapterType);
+
+ ///
+ /// Проверить, зарегистрирован ли адаптер с указанным ID.
+ ///
+ bool Contains(string adapterId);
+
+ ///
+ /// Удалить адаптер по ID.
+ ///
+ bool Remove(string adapterId);
+}
\ No newline at end of file
diff --git a/BotPages.Core/Abstractions/MessageInfo.cs b/BotPages.Core/Abstractions/MessageInfo.cs
new file mode 100644
index 0000000..b629ab7
--- /dev/null
+++ b/BotPages.Core/Abstractions/MessageInfo.cs
@@ -0,0 +1,34 @@
+namespace BotPages.Core.Abstractions;
+
+///
+/// Информация о сообщении.
+///
+public class MessageInfo
+{
+ /// ID сообщения.
+ public required string MessageId { get; init; }
+
+ /// ID чата.
+ public required string ChatId { get; init; }
+
+ /// Текст сообщения.
+ public string? Text { get; init; }
+
+ /// Формат текста.
+ public MessageFormat? Format { get; init; }
+
+ /// Дата отправки.
+ public DateTime Date { get; init; }
+
+ /// ID отправителя.
+ public string? FromUserId { get; init; }
+
+ /// Закреплено ли сообщение.
+ public bool IsPinned { get; init; }
+
+ /// Является ли ответом на другое сообщение.
+ public bool IsReply { get; init; }
+
+ /// ID сообщения, на которое отвечает.
+ public string? ReplyToMessageId { get; init; }
+}
diff --git a/BotPages.Core/Abstractions/MessengerAdapterBase.cs b/BotPages.Core/Abstractions/MessengerAdapterBase.cs
new file mode 100644
index 0000000..2240527
--- /dev/null
+++ b/BotPages.Core/Abstractions/MessengerAdapterBase.cs
@@ -0,0 +1,102 @@
+using BotPages.Core.Context;
+using BotPages.Core.Messaging;
+
+namespace BotPages.Core.Abstractions;
+
+///
+/// Базовый класс для адаптеров мессенджеров.
+///
+public abstract class MessengerAdapterBase : IMessengerAdapterSetup
+{
+ ///
+ /// Уникальный идентификатор адаптера.
+ ///
+ public string AdapterId { get; internal set; } = string.Empty;
+
+ ///
+ /// Тип адаптера (Telegram, VK, WhatsApp и т.д.).
+ ///
+ public abstract string AdapterType { get; }
+
+ ///
+ /// Название адаптера для отображения.
+ ///
+ public string DisplayName { get; set; } = string.Empty;
+
+ ///
+ /// Доступные возможности мессенджера.
+ ///
+ public abstract Capabilities Capabilities { get; }
+
+ ///
+ /// Универсальный метод отправки с использованием общего описания запроса.
+ ///
+ public abstract Task SendAsync(SendRequest request, CancellationToken ct = default);
+
+ ///
+ /// Универсальный метод удаления сообщения.
+ ///
+ public abstract Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default);
+
+ ///
+ /// Удалить несколько сообщений за раз.
+ ///
+ public abstract Task DeleteMultipleAsync(string chatId, IEnumerable messageIds, CancellationToken ct = default);
+
+ ///
+ /// Редактировать только текст сообщения.
+ ///
+ public abstract Task EditTextAsync(string chatId, string messageId, string text,
+ MessageFormat? format = null, CancellationToken ct = default);
+
+ ///
+ /// Редактировать только клавиатуру сообщения.
+ ///
+ public abstract Task EditButtonsAsync(string chatId, string messageId,
+ IEnumerable>? inlineButtons = null,
+ CancellationToken ct = default);
+
+ ///
+ /// Закрепить сообщение в чате.
+ ///
+ public abstract Task PinMessageAsync(string chatId, string messageId, bool disableNotification = false,
+ CancellationToken ct = default);
+
+ ///
+ /// Открепить сообщение в чате.
+ ///
+ public abstract Task UnpinMessageAsync(string chatId, string messageId, CancellationToken ct = default);
+
+ ///
+ /// Получить информацию о сообщении.
+ ///
+ public abstract Task GetMessageInfoAsync(string chatId, string messageId, CancellationToken ct = default);
+
+ ///
+ /// Переслать сообщение.
+ ///
+ public abstract Task ForwardMessageAsync(string fromChatId, string messageId, string toChatId,
+ bool disableNotification = false, CancellationToken ct = default);
+
+ ///
+ /// Копировать сообщение с возможностью редактирования.
+ ///
+ public abstract Task CopyMessageAsync(string fromChatId, string messageId, string toChatId,
+ string? caption = null, MessageFormat? captionFormat = null,
+ bool disableNotification = false, CancellationToken ct = default);
+
+ ///
+ /// Создать билдер альбома для отправки медиагруппы.
+ ///
+ public abstract IAlbumBuilder CreateAlbumBuilder(PageContext ctx);
+
+ ///
+ /// Вызывается при выходе со страницы.
+ ///
+ public abstract Task OnLeaveAsync(PageContext ctx, CancellationToken ct);
+
+ ///
+ /// Запуск работы адаптера.
+ ///
+ public abstract Task StartAdapterAsync(Func onUpdate, List commands, CancellationToken ct);
+}
\ No newline at end of file
diff --git a/BotPages.Core/Abstractions/MultiAdapterFactory.cs b/BotPages.Core/Abstractions/MultiAdapterFactory.cs
index ddf47fd..ec3ac10 100644
--- a/BotPages.Core/Abstractions/MultiAdapterFactory.cs
+++ b/BotPages.Core/Abstractions/MultiAdapterFactory.cs
@@ -1,50 +1,142 @@
namespace BotPages.Core.Abstractions;
-
///
-/// Реализация , позволяющая регистрировать и разрешать несколько адаптеров мессенджеров.
+/// Реализация , позволяющая регистрировать и разрешать несколько адаптеров.
///
public sealed class MultiAdapterFactory : IMessengerAdapterFactory
{
- private readonly Dictionary _adapters = new(StringComparer.OrdinalIgnoreCase);
+ private readonly List _allAdapters = new();
+ private readonly Dictionary _adaptersById = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary> _adaptersByType = new(StringComparer.OrdinalIgnoreCase);
///
- /// Список зарегистрированных адаптеров.
+ /// Список всех зарегистрированных адаптеров.
///
- public Dictionary Adapters => _adapters;
+ public IReadOnlyList AllAdapters => _allAdapters;
///
- /// Зарегистрировать адаптер для указанного типа мессенджера.
+ /// Зарегистрировать адаптер с уникальным идентификатором.
///
- ///
- /// Тип мессенджера (например, "Telegram", "Slack", "VK").
- ///
- ///
- /// Экземпляр адаптера, реализующий .
- ///
- ///
- /// Текущий экземпляр для цепочки вызовов.
- ///
- 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();
+ _adaptersByType[adapterType] = typeList;
+ }
+ typeList.Add(adapter);
+
return this;
}
///
- /// Получить адаптер для указанного мессенджера.
+ /// Зарегистрировать адаптер с автоматически сгенерированным ID.
///
- ///
- /// Тип мессенджера (например, "Telegram", "Slack", "VK").
- ///
- ///
- /// Экземпляр , зарегистрированный для данного типа мессенджера.
- ///
- ///
- /// Выбрасывается, если адаптер для указанного типа не зарегистрирован.
- ///
- 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);
+ }
+
+ ///
+ /// Получить адаптер по ID.
+ ///
+ public IMessengerAdapter Resolve(string adapterId)
+ {
+ if (_adaptersById.TryGetValue(adapterId, out var adapter))
+ {
+ return adapter;
+ }
+
+ throw new InvalidOperationException($"No adapter registered with ID '{adapterId}'");
+ }
+
+ ///
+ /// Попытаться получить адаптер по ID.
+ ///
+ public bool TryResolve(string adapterId, out IMessengerAdapter? adapter)
+ {
+ var result = _adaptersById.TryGetValue(adapterId, out var adapterSetup);
+ adapter = adapterSetup;
+ return result;
+ }
+
+ ///
+ /// Получить все адаптеры определенного типа.
+ ///
+ public IReadOnlyList GetAdaptersByType(string adapterType)
+ {
+ if (_adaptersByType.TryGetValue(adapterType, out var adapters))
+ {
+ return adapters.AsReadOnly();
+ }
+
+ return Array.Empty();
+ }
+
+ ///
+ /// Проверить, зарегистрирован ли адаптер с указанным ID.
+ ///
+ public bool Contains(string adapterId) => _adaptersById.ContainsKey(adapterId);
+
+ ///
+ /// Удалить адаптер по ID.
+ ///
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/BotPages.Core/Abstractions/SendRequest.cs b/BotPages.Core/Abstractions/SendRequest.cs
index d009c97..a63b377 100644
--- a/BotPages.Core/Abstractions/SendRequest.cs
+++ b/BotPages.Core/Abstractions/SendRequest.cs
@@ -39,4 +39,66 @@ public sealed class SendRequest
/// / .
///
public AdapterOptionsBag? AdapterOptions { get; init; }
+
+ ///
+ /// ID , .
+ ///
+ public string? ReplyToMessageId { get; init; }
+
+ ///
+ /// .
+ ///
+ public bool QuoteReply { get; init; } = true;
+
+ ///
+ /// ( ).
+ ///
+ public string? QuoteTitle { get; init; }
+
+ ///
+ /// .
+ ///
+ public bool DisableWebPagePreview { get; init; } = false;
+
+ ///
+ /// .
+ ///
+ public bool DisableNotification { get; init; } = false;
+
+ ///
+ /// .
+ ///
+ public bool ProtectContent { get; init; } = false;
+
+ ///
+ /// ( ).
+ ///
+ public MessageStyle? Style { get; init; }
+
+ ///
+ /// ( ).
+ ///
+ public DateTime? ScheduleDate { get; init; }
+
+ ///
+ /// ( ).
+ ///
+ public string? Topic { get; init; }
}
+
+///
+/// .
+///
+public enum MessageStyle
+{
+ /// .
+ Default,
+ /// .
+ Heading,
+ /// .
+ Warning,
+ /// .
+ Success,
+ /// .
+ Error
+}
\ No newline at end of file
diff --git a/BotPages.Core/BotPagesApp.cs b/BotPages.Core/BotPagesApp.cs
index 79174e7..069bf3b 100644
--- a/BotPages.Core/BotPagesApp.cs
+++ b/BotPages.Core/BotPagesApp.cs
@@ -38,14 +38,40 @@ public sealed class BotPagesApp
}
///
- /// Добавить адаптер.
+ /// Добавить адаптер с указанием ID.
///
- public BotPagesApp AddAdapter(string messengerType, IMessengerAdapterSetup adapter)
+ /// Если адаптер с таким ID уже существует.
+ public BotPagesApp AddAdapter(string adapterId, IMessengerAdapterSetup adapter)
{
- _adapterFactory.Register(messengerType, adapter);
+ _adapterFactory.Register(adapterId, adapter);
return this;
}
+ ///
+ /// Добавить адаптер с автоматическим ID.
+ ///
+ public BotPagesApp AddAdapter(IMessengerAdapterSetup adapter)
+ {
+ _adapterFactory.Register(adapter);
+ return this;
+ }
+
+ ///
+ /// Проверить, существует ли адаптер с указанным ID.
+ ///
+ public bool HasAdapter(string adapterId) => _adapterFactory.Contains(adapterId);
+
+ ///
+ /// Получить адаптер по ID.
+ ///
+ public IMessengerAdapter GetAdapter(string adapterId) => _adapterFactory.Resolve(adapterId);
+
+ ///
+ /// Получить все адаптеры определенного типа.
+ ///
+ public IReadOnlyList GetAdaptersByType(string adapterType)
+ => _adapterFactory.GetAdaptersByType(adapterType);
+
///
/// Установить страницу по умолчанию.
///
@@ -174,6 +200,28 @@ public sealed class BotPagesApp
await pipeline();
}
+ ///
+ /// Создать контекст страницы для текущего обновления.
+ ///
+ private async Task CreatePageContextAsync(UpdateContext update, CancellationToken ct)
+ {
+ _currentCt = ct;
+
+ var sessionKey = CompositeSessionKey.FromUpdate(update);
+
+ var ctx = new PageContext
+ {
+ Update = update,
+ SessionKey = sessionKey,
+ StateStorage = _state,
+ Navigation = _navigation,
+ Adapter = _adapterFactory.Resolve(update.AdapterId),
+ AdapterFactory = _adapterFactory,
+ };
+
+ return await Task.FromResult(ctx);
+ }
+
private Func BuildPipeline(PageContext ctx, Func terminal)
{
Func next = terminal;
@@ -188,27 +236,6 @@ public sealed class BotPagesApp
// Технические поля для конвейера
private CancellationToken _currentCt;
- ///
- /// Создать контекст страницы для текущего обновления.
- ///
- private async Task CreatePageContextAsync(UpdateContext update, CancellationToken ct)
- {
- _currentCt = ct;
-
- var sessionKey = new CompositeSessionKey(update.MessengerType, update.Chat.Id, update.User.Id);
-
- var ctx = new PageContext
- {
- Update = update,
- SessionKey = sessionKey,
- StateStorage = _state,
- Navigation = _navigation,
- Adapter = _adapterFactory.Resolve(update.MessengerType),
- };
-
- return await Task.FromResult(ctx);
- }
-
///
/// Отправить обновление на текущую страницу.
///
@@ -230,6 +257,8 @@ public sealed class BotPagesApp
if (update.Kind.HasFlag(UpdateKind.Text) && update.Text is not null) await page.OnText(ctx, update.Text, ct);
if (update.Kind.HasFlag(UpdateKind.Button) && update.Text is not null) await page.OnButton(ctx, update.Text, ct);
if (update.Kind.HasFlag(UpdateKind.File) && update.Files.Count > 0) await page.OnFile(ctx, update.Files, ct);
+ if (update.Kind.HasFlag(UpdateKind.Pin) && update.PinInfo is not null) await page.OnPin(ctx, update.PinInfo, ct);
+ if (update.Kind.HasFlag(UpdateKind.Delete) && update.DeleteInfo is not null) await page.OnDelete(ctx, update.DeleteInfo, ct);
}
catch (Exception ex)
@@ -252,9 +281,9 @@ public sealed class BotPagesApp
///
public async Task Build(CancellationToken cancellationToken)
{
- foreach (var adapter in _adapterFactory.Adapters)
+ foreach (var adapter in _adapterFactory.AllAdapters)
{
- await adapter.Value.StartAdapterAsync(update => HandleUpdateAsync(update, cancellationToken), _commands.Commands, cancellationToken);
+ await adapter.StartAdapterAsync(update => HandleUpdateAsync(update, cancellationToken), _commands.Commands, cancellationToken);
}
}
}
\ No newline at end of file
diff --git a/BotPages.Core/Context/DeleteInfo.cs b/BotPages.Core/Context/DeleteInfo.cs
new file mode 100644
index 0000000..be3d400
--- /dev/null
+++ b/BotPages.Core/Context/DeleteInfo.cs
@@ -0,0 +1,16 @@
+namespace BotPages.Core.Context;
+
+///
+/// Информация об удалении сообщения.
+///
+public class DeleteInfo
+{
+ /// ID удаленного сообщения.
+ public required string MessageId { get; init; }
+
+ /// Дата удаления.
+ public DateTime DeleteDate { get; init; }
+
+ /// Удалено ли массово.
+ public bool IsBulkDelete { get; init; }
+}
diff --git a/BotPages.Core/Context/EditInfo.cs b/BotPages.Core/Context/EditInfo.cs
new file mode 100644
index 0000000..43c53f8
--- /dev/null
+++ b/BotPages.Core/Context/EditInfo.cs
@@ -0,0 +1,16 @@
+namespace BotPages.Core.Context;
+
+///
+/// Информация о редактировании сообщения.
+///
+public class EditInfo
+{
+ /// Новый текст сообщения.
+ public string? NewText { get; init; }
+
+ /// Дата редактирования.
+ public DateTime EditDate { get; init; }
+
+ /// Были ли изменены кнопки.
+ public bool ButtonsChanged { get; init; }
+}
diff --git a/BotPages.Core/Context/PageContext.cs b/BotPages.Core/Context/PageContext.cs
index 6ae3c77..f00ad13 100644
--- a/BotPages.Core/Context/PageContext.cs
+++ b/BotPages.Core/Context/PageContext.cs
@@ -16,9 +16,14 @@ public sealed class PageContext
/// Хранилище состояния.
public required IStateStorage StateStorage { get; init; }
+
/// Сервис навигации.
public required NavigationService Navigation { get; init; }
- /// Адаптер мессенджера.
+
+ /// Фабрика адаптеров (для получения других адаптеров).
+ public required IMessengerAdapterFactory AdapterFactory { get; init; }
+
+ /// Текущий адаптер мессенджера.
public required IMessengerAdapter Adapter { get; init; }
///
@@ -26,4 +31,36 @@ public sealed class PageContext
///
public IAlbumBuilder Albums => Adapter.CreateAlbumBuilder(this);
+ ///
+ /// Получить адаптер по ID.
+ ///
+ public IMessengerAdapter GetAdapter(string adapterId)
+ => AdapterFactory.Resolve(adapterId);
+
+ ///
+ /// Попытаться получить адаптер по ID.
+ ///
+ public bool TryGetAdapter(string adapterId, out IMessengerAdapter? adapter)
+ => AdapterFactory.TryResolve(adapterId, out adapter);
+
+ ///
+ /// Получить все адаптеры определенного типа.
+ ///
+ public IReadOnlyList GetAdaptersByType(string adapterType)
+ => AdapterFactory.GetAdaptersByType(adapterType);
+
+ ///
+ /// Получить текущий тип адаптера.
+ ///
+ public string CurrentAdapterType => Update.AdapterType;
+
+ ///
+ /// Получить текущий ID адаптера.
+ ///
+ public string CurrentAdapterId => Update.AdapterId;
+
+ ///
+ /// Получить все адаптеры.
+ ///
+ public IReadOnlyList AllAdapters => AdapterFactory.AllAdapters;
}
\ No newline at end of file
diff --git a/BotPages.Core/Context/PageContextAdapterExtensions.cs b/BotPages.Core/Context/PageContextAdapterExtensions.cs
index 5510f69..2970828 100644
--- a/BotPages.Core/Context/PageContextAdapterExtensions.cs
+++ b/BotPages.Core/Context/PageContextAdapterExtensions.cs
@@ -1,4 +1,5 @@
using BotPages.Core.Abstractions;
+using BotPages.Core.Messaging;
namespace BotPages.Core;
@@ -19,4 +20,108 @@ public static class PageContextAdapterExtensions
///
public static Task DeleteAsync(this PageContext ctx, string chatId, string messageId, CancellationToken ct = default)
=> ctx.Adapter.DeleteAsync(chatId, messageId, ct);
+
+ ///
+ /// Удалить сообщение (используя ChatId из контекста).
+ ///
+ public static Task DeleteAsync(this PageContext ctx, string messageId, CancellationToken ct = default)
+ => ctx.Adapter.DeleteAsync(ctx.Update.Chat.Id, messageId, ct);
+
+ ///
+ /// Удалить несколько сообщений.
+ ///
+ public static Task DeleteMultipleAsync(this PageContext ctx, string chatId, IEnumerable messageIds, CancellationToken ct = default)
+ => ctx.Adapter.DeleteMultipleAsync(chatId, messageIds, ct);
+
+ ///
+ /// Удалить несколько сообщений (используя ChatId из контекста).
+ ///
+ public static Task DeleteMultipleAsync(this PageContext ctx, IEnumerable messageIds, CancellationToken ct = default)
+ => ctx.Adapter.DeleteMultipleAsync(ctx.Update.Chat.Id, messageIds, ct);
+
+ ///
+ /// Редактировать текст сообщения.
+ ///
+ public static Task EditTextAsync(this PageContext ctx, string chatId, string messageId, string text,
+ MessageFormat? format = null, CancellationToken ct = default)
+ => ctx.Adapter.EditTextAsync(chatId, messageId, text, format, ct);
+
+ ///
+ /// Редактировать текст сообщения (используя ChatId из контекста).
+ ///
+ public static Task EditTextAsync(this PageContext ctx, string messageId, string text,
+ MessageFormat? format = null, CancellationToken ct = default)
+ => ctx.Adapter.EditTextAsync(ctx.Update.Chat.Id, messageId, text, format, ct);
+
+ ///
+ /// Редактировать кнопки сообщения.
+ ///
+ public static Task EditButtonsAsync(this PageContext ctx, string chatId, string messageId,
+ IEnumerable>? inlineButtons = null, CancellationToken ct = default)
+ => ctx.Adapter.EditButtonsAsync(chatId, messageId, inlineButtons, ct);
+
+ ///
+ /// Редактировать кнопки сообщения (используя ChatId из контекста).
+ ///
+ public static Task EditButtonsAsync(this PageContext ctx, string messageId,
+ IEnumerable>? inlineButtons = null, CancellationToken ct = default)
+ => ctx.Adapter.EditButtonsAsync(ctx.Update.Chat.Id, messageId, inlineButtons, ct);
+
+ ///
+ /// Закрепить сообщение.
+ ///
+ public static Task PinMessageAsync(this PageContext ctx, string chatId, string messageId,
+ bool disableNotification = false, CancellationToken ct = default)
+ => ctx.Adapter.PinMessageAsync(chatId, messageId, disableNotification, ct);
+
+ ///
+ /// Закрепить сообщение (используя ChatId из контекста).
+ ///
+ public static Task PinMessageAsync(this PageContext ctx, string messageId,
+ bool disableNotification = false, CancellationToken ct = default)
+ => ctx.Adapter.PinMessageAsync(ctx.Update.Chat.Id, messageId, disableNotification, ct);
+
+ ///
+ /// Открепить сообщение.
+ ///
+ public static Task UnpinMessageAsync(this PageContext ctx, string chatId, string messageId,
+ CancellationToken ct = default)
+ => ctx.Adapter.UnpinMessageAsync(chatId, messageId, ct);
+
+ ///
+ /// Открепить сообщение (используя ChatId из контекста).
+ ///
+ public static Task UnpinMessageAsync(this PageContext ctx, string messageId,
+ CancellationToken ct = default)
+ => ctx.Adapter.UnpinMessageAsync(ctx.Update.Chat.Id, messageId, ct);
+
+ ///
+ /// Получить информацию о сообщении.
+ ///
+ public static Task GetMessageInfoAsync(this PageContext ctx, string chatId, string messageId,
+ CancellationToken ct = default)
+ => ctx.Adapter.GetMessageInfoAsync(chatId, messageId, ct);
+
+ ///
+ /// Получить информацию о сообщении (используя ChatId из контекста).
+ ///
+ public static Task GetMessageInfoAsync(this PageContext ctx, string messageId,
+ CancellationToken ct = default)
+ => ctx.Adapter.GetMessageInfoAsync(ctx.Update.Chat.Id, messageId, ct);
+
+ ///
+ /// Переслать сообщение.
+ ///
+ public static Task ForwardMessageAsync(this PageContext ctx, string fromChatId, string messageId,
+ string toChatId, bool disableNotification = false, CancellationToken ct = default)
+ => ctx.Adapter.ForwardMessageAsync(fromChatId, messageId, toChatId, disableNotification, ct);
+
+ ///
+ /// Копировать сообщение.
+ ///
+ public static Task CopyMessageAsync(this PageContext ctx, string fromChatId, string messageId,
+ string toChatId, string? caption = null, MessageFormat? captionFormat = null,
+ bool disableNotification = false, CancellationToken ct = default)
+ => ctx.Adapter.CopyMessageAsync(fromChatId, messageId, toChatId, caption, captionFormat,
+ disableNotification, ct);
}
\ No newline at end of file
diff --git a/BotPages.Core/Context/PinInfo.cs b/BotPages.Core/Context/PinInfo.cs
new file mode 100644
index 0000000..cb6e2e4
--- /dev/null
+++ b/BotPages.Core/Context/PinInfo.cs
@@ -0,0 +1,16 @@
+namespace BotPages.Core.Context;
+
+///
+/// Информация о закреплении сообщения.
+///
+public class PinInfo
+{
+ /// ID закрепленного сообщения.
+ public required string MessageId { get; init; }
+
+ /// Дата закрепления.
+ public DateTime PinDate { get; init; }
+
+ /// Отключено ли уведомление.
+ public bool NotificationDisabled { get; init; }
+}
\ No newline at end of file
diff --git a/BotPages.Core/Context/UpdateContext.cs b/BotPages.Core/Context/UpdateContext.cs
index ce54a0c..a8f9014 100644
--- a/BotPages.Core/Context/UpdateContext.cs
+++ b/BotPages.Core/Context/UpdateContext.cs
@@ -1,25 +1,6 @@
-namespace BotPages.Core.Context;
+using BotPages.Core.Abstractions;
-using BotPages.Core.Abstractions;
-
-///
-/// Тип входящего обновления.
-///
-[Flags]
-public enum UpdateKind
-{
- /// Неизвестное сообщение.
- None = 0,
-
- /// Текстовое сообщение.
- Text = 1 << 0,
-
- /// Файлы (один или несколько).
- File = 1 << 1,
-
- /// Нажатие кнопки.
- Button = 1 << 2,
-}
+namespace BotPages.Core.Context;
///
/// Контекст входящего обновления от мессенджера.
@@ -27,8 +8,11 @@ public enum UpdateKind
///
public sealed class UpdateContext
{
- /// Тип мессенджера.
- public required string MessengerType { get; init; }
+ /// Идентификатор адаптера, от которого пришло обновление.
+ public required string AdapterId { get; init; }
+
+ /// Тип адаптера (определяется адаптером).
+ public required string AdapterType { get; init; }
///
/// Данные пользователя, от которого пришло обновление.
@@ -51,9 +35,34 @@ public sealed class UpdateContext
///
public string? Text { get; init; }
+ ///
+ /// ID сообщения, к которому относится обновление.
+ ///
+ public string? MessageId { get; init; }
+
+ ///
+ /// ID сообщения, на которое дан ответ (если есть).
+ ///
+ public string? ReplyToMessageId { get; init; }
+
///
/// Список файлов, если Kind = File.
/// Может содержать один или несколько файлов.
///
public List Files { get; init; } = new();
+
+ ///
+ /// Информация о редактировании (если Kind = Edit).
+ ///
+ public EditInfo? EditInfo { get; init; }
+
+ ///
+ /// Информация об удалении (если Kind = Delete).
+ ///
+ public DeleteInfo? DeleteInfo { get; init; }
+
+ ///
+ /// Информация о закреплении (если Kind = Pin).
+ ///
+ public PinInfo? PinInfo { get; init; }
}
diff --git a/BotPages.Core/Context/UpdateKind.cs b/BotPages.Core/Context/UpdateKind.cs
new file mode 100644
index 0000000..f448749
--- /dev/null
+++ b/BotPages.Core/Context/UpdateKind.cs
@@ -0,0 +1,32 @@
+namespace BotPages.Core.Context;
+
+///
+/// Тип входящего обновления.
+///
+[Flags]
+public enum UpdateKind
+{
+ /// Неизвестное сообщение.
+ None = 0,
+
+ /// Текстовое сообщение.
+ Text = 1 << 0,
+
+ /// Файлы (один или несколько).
+ File = 1 << 1,
+
+ /// Нажатие кнопки.
+ Button = 1 << 2,
+
+ /// Редактирование сообщения.
+ Edit = 1 << 3,
+
+ /// Удаление сообщения.
+ Delete = 1 << 4,
+
+ /// Закрепление сообщения.
+ Pin = 1 << 5,
+
+ /// Ответ на сообщение.
+ Reply = 1 << 6,
+}
diff --git a/BotPages.Core/Messaging/MessageBuilder.cs b/BotPages.Core/Messaging/MessageBuilder.cs
index 5048c18..ace6636 100644
--- a/BotPages.Core/Messaging/MessageBuilder.cs
+++ b/BotPages.Core/Messaging/MessageBuilder.cs
@@ -4,7 +4,7 @@ namespace BotPages.Core.Messaging;
///
/// Fluent‑билдер для отправки сообщений (текст, кнопки, файлы, альбомы, прогресс).
-/// Поддерживает указание адаптер-специфичных опций через `WithAdapterOption`.
+/// Поддерживает указание адаптер-специфичных опций через .
///
public sealed class MessageBuilder
{
@@ -17,11 +17,77 @@ public sealed class MessageBuilder
private readonly List<(FileDescriptor file, string? caption, MessageFormat? captionFormat)> _album = new();
private bool _disableReplyKeyboard;
private AdapterOptionsBag? _adapterOptions = null;
- private string? _chatId = null;
+ private string? _replyToMessageId = null;
+ private bool _quoteReply = true;
+ private string? _quoteTitle = null;
+ private bool _disableWebPagePreview = false;
+ private bool _disableNotification = false;
+ private bool _protectContent = false;
+ private MessageStyle? _style = null;
+ private DateTime? _scheduleDate = null;
+ private string? _topic = null;
+ private string? _messageId = null;
+ private IMessengerAdapter? _targetAdapter = null;
+ private string? _targetChatId = null;
/// Создать билдер сообщений.
public MessageBuilder(PageContext ctx) => _ctx = ctx;
+ ///
+ /// Отправить сообщение через указанный адаптер.
+ ///
+ public MessageBuilder WithAdapter(IMessengerAdapter adapter)
+ {
+ _targetAdapter = adapter;
+ return this;
+ }
+ ///
+ /// Отправить сообщение через адаптер по ID.
+ ///
+ public MessageBuilder WithAdapter(string adapterId)
+ {
+ _targetAdapter = _ctx.AdapterFactory.Resolve(adapterId);
+ return this;
+ }
+
+ ///
+ /// Использовать текущий адаптер (по умолчанию).
+ ///
+ public MessageBuilder UseCurrentAdapter()
+ {
+ _targetAdapter = null;
+ return this;
+ }
+
+ ///
+ /// Отправить в указанный чат (с указанием адаптера).
+ ///
+ public MessageBuilder ToChat(string chatId, IMessengerAdapter? adapter)
+ {
+ _targetChatId = chatId;
+ _targetAdapter = adapter;
+ return this;
+ }
+
+ ///
+ /// Отправить в указанный чат через адаптер по ID.
+ ///
+ public MessageBuilder ToChat(string chatId, string adapterId)
+ {
+ _targetChatId = chatId;
+ _targetAdapter = _ctx.AdapterFactory.Resolve(adapterId);
+ return this;
+ }
+
+ ///
+ /// Отправить в указанный чат.
+ ///
+ public MessageBuilder ToChat(string chatId)
+ {
+ _targetChatId = chatId;
+ return this;
+ }
+
///
/// Установить опции для конкретного адаптера. Ключ адаптера определяется адаптером (напр., "telegram").
///
@@ -32,13 +98,6 @@ public sealed class MessageBuilder
return this;
}
- /// ID чата куда отрпавить сообщение.
- public MessageBuilder ChatId(string chatId)
- {
- _chatId = chatId;
- return this;
- }
-
/// Текст сообщения.
public MessageBuilder Text(string text, MessageFormat format = MessageFormat.Plain)
{
@@ -47,6 +106,71 @@ public sealed class MessageBuilder
return this;
}
+ /// Стиль сообщения.
+ public MessageBuilder Style(MessageStyle style)
+ {
+ _style = style;
+ return this;
+ }
+
+ /// Тема сообщения (для форумов).
+ public MessageBuilder Topic(string topic)
+ {
+ _topic = topic;
+ return this;
+ }
+
+ /// Запланировать отправку на определенную дату.
+ public MessageBuilder Schedule(DateTime scheduleDate)
+ {
+ _scheduleDate = scheduleDate;
+ return this;
+ }
+
+ /// Отключить предпросмотр ссылок.
+ public MessageBuilder DisableWebPagePreview()
+ {
+ _disableWebPagePreview = true;
+ return this;
+ }
+
+ /// Отключить уведомление.
+ public MessageBuilder DisableNotification()
+ {
+ _disableNotification = true;
+ return this;
+ }
+
+ /// Защитить содержимое от пересылки.
+ public MessageBuilder ProtectContent()
+ {
+ _protectContent = true;
+ return this;
+ }
+
+ /// Ответить на сообщение.
+ public MessageBuilder ReplyTo(string messageId, bool quote = true, string? quoteTitle = null)
+ {
+ _replyToMessageId = messageId;
+ _quoteReply = quote;
+ _quoteTitle = quoteTitle;
+ return this;
+ }
+
+ /// Ответить на текущее сообщение.
+ public MessageBuilder ReplyToCurrent(bool quote = true, string? quoteTitle = null)
+ {
+ if (_ctx.Update is not null)
+ {
+ // Предполагаем, что Update содержит ID текущего сообщения
+ // Это может потребовать расширения UpdateContext
+ _replyToMessageId = _ctx.Update.MessageId;
+ _quoteReply = quote;
+ _quoteTitle = quoteTitle;
+ }
+ return this;
+ }
+
/// Добавить inline‑кнопку.
public MessageBuilder Inline(string label, string value)
{
@@ -132,7 +256,104 @@ public sealed class MessageBuilder
/// Удалить сообщение.
public async Task DeleteAsync(string messageId, CancellationToken ct = default)
{
- await _ctx.DeleteAsync(this._chatId ?? _ctx.Update.Chat.Id, messageId, ct);
+ await _ctx.Adapter.DeleteAsync(GetEffectiveChatId(), messageId, ct);
+ }
+
+ /// Удалить несколько сообщений.
+ public async Task DeleteMultipleAsync(IEnumerable messageIds, CancellationToken ct = default)
+ {
+ return await _ctx.Adapter.DeleteMultipleAsync(GetEffectiveChatId(), messageIds, ct);
+ }
+
+ /// Редактировать только текст сообщения.
+ public async Task EditTextAsync(string messageId, string newText,
+ MessageFormat? format = null, CancellationToken ct = default)
+ {
+ return await _ctx.Adapter.EditTextAsync(
+ GetEffectiveChatId(),
+ messageId,
+ newText,
+ format ?? _format,
+ ct);
+ }
+
+ /// Редактировать только кнопки сообщения.
+ public async Task EditButtonsAsync(string messageId,
+ IEnumerable>? inlineButtons = null,
+ CancellationToken ct = default)
+ {
+ return await _ctx.Adapter.EditButtonsAsync(
+ GetEffectiveChatId(),
+ messageId,
+ inlineButtons ?? _inline,
+ ct);
+ }
+
+ /// Закрепить сообщение.
+ public async Task PinAsync(string messageId, bool disableNotification = false,
+ CancellationToken ct = default)
+ {
+ return await _ctx.Adapter.PinMessageAsync(
+ GetEffectiveChatId(),
+ messageId,
+ disableNotification,
+ ct);
+ }
+
+ /// Открепить сообщение.
+ public async Task UnpinAsync(string messageId, CancellationToken ct = default)
+ {
+ return await _ctx.Adapter.UnpinMessageAsync(
+ GetEffectiveChatId(),
+ messageId,
+ ct);
+ }
+
+ /// Получить информацию о сообщении.
+ public async Task GetInfoAsync(string messageId, CancellationToken ct = default)
+ {
+ return await _ctx.Adapter.GetMessageInfoAsync(
+ GetEffectiveChatId(),
+ messageId,
+ ct);
+ }
+
+ /// Переслать сообщение.
+ public async Task ForwardAsync(string fromChatId, string messageId, string toChatId,
+ bool disableNotification = false, CancellationToken ct = default)
+ {
+ return await _ctx.Adapter.ForwardMessageAsync(
+ fromChatId,
+ messageId,
+ toChatId ?? GetEffectiveChatId(),
+ disableNotification,
+ ct);
+ }
+
+ /// Копировать сообщение.
+ public async Task CopyAsync(string fromChatId, string messageId, string toChatId,
+ string? caption = null, MessageFormat? captionFormat = null,
+ bool disableNotification = false, CancellationToken ct = default)
+ {
+ return await _ctx.Adapter.CopyMessageAsync(
+ fromChatId,
+ messageId,
+ toChatId ?? GetEffectiveChatId(),
+ caption,
+ captionFormat,
+ disableNotification,
+ ct);
+ }
+
+ private string GetEffectiveChatId()
+ {
+ return _targetChatId ?? _ctx.Update.Chat.Id;
+ }
+
+ private IMessengerAdapter GetEffectiveAdapter()
+ {
+ return _targetAdapter ?? _ctx.Adapter
+ ?? throw new InvalidOperationException("No adapter specified for sending message");
}
/// Отправить собранное сообщение.
@@ -142,27 +363,43 @@ public sealed class MessageBuilder
/// Редактировать сообщение.
public async Task SendAsync(string messageId, CancellationToken ct = default)
{
+ _messageId = string.IsNullOrWhiteSpace(messageId) ? null : messageId;
string? outMessageId = null;
List>? reply = null;
if (_disableReplyKeyboard) reply = new();
else if (_reply.Any()) reply = _reply;
+ // Определяем целевой адаптер
+ var adapter = GetEffectiveAdapter();
+
+ // Определяем целевой чат
+ var targetChatId = GetEffectiveChatId();
+
// Текст
if (!string.IsNullOrWhiteSpace(_text))
{
var req = new SendRequest
{
- ChatId = _chatId ?? _ctx.Update.Chat.Id,
+ ChatId = targetChatId,
Text = _text,
TextFormat = _format,
Inline = _inline,
Reply = reply,
- MessageId = string.IsNullOrWhiteSpace(messageId) ? null : messageId,
+ MessageId = _messageId,
+ ReplyToMessageId = _replyToMessageId,
+ QuoteReply = _quoteReply,
+ QuoteTitle = _quoteTitle,
+ DisableWebPagePreview = _disableWebPagePreview,
+ DisableNotification = _disableNotification,
+ ProtectContent = _protectContent,
+ Style = _style,
+ ScheduleDate = _scheduleDate,
+ Topic = _topic,
AdapterOptions = _adapterOptions
};
- outMessageId = await _ctx.SendAsync(req, ct);
+ outMessageId = await adapter.SendAsync(req, ct);
}
// Файлы
@@ -170,16 +407,25 @@ public sealed class MessageBuilder
{
var req = new SendRequest
{
- ChatId = _chatId ?? _ctx.Update.Chat.Id,
+ ChatId = targetChatId,
File = file,
Caption = caption,
CaptionFormat = captionFormat,
Inline = _inline,
Reply = reply,
+ ReplyToMessageId = _replyToMessageId,
+ QuoteReply = _quoteReply,
+ QuoteTitle = _quoteTitle,
+ DisableWebPagePreview = _disableWebPagePreview,
+ DisableNotification = _disableNotification,
+ ProtectContent = _protectContent,
+ Style = _style,
+ ScheduleDate = _scheduleDate,
+ Topic = _topic,
AdapterOptions = _adapterOptions
};
- var res = await _ctx.SendAsync(req, ct);
+ var res = await adapter.SendAsync(req, ct);
// сохранить первый возвращённый id сообщения
if (outMessageId is null && res is not null) outMessageId = res;
}
@@ -187,22 +433,58 @@ public sealed class MessageBuilder
// Альбом
if (_album.Count > 0)
{
- var builder = _ctx.Albums;
+ // Для альбома нужен PageContext с правильным адаптером
+ var albumCtx = CreateAlbumContext(_ctx, adapter);
+ var builder = adapter.CreateAlbumBuilder(albumCtx);
foreach (var (file, caption, captionFormat) in _album)
builder.Add(file, caption, captionFormat);
await builder.SendAsync(ct);
}
- _text = null;
- _files.Clear();
- _album.Clear();
-
- _adapterOptions = null;
- _chatId = null;
-
- _reply.Clear();
- _inline.Clear();
+ Reset();
return outMessageId;
}
-}
+
+ private PageContext CreateAlbumContext(PageContext originalCtx, IMessengerAdapter adapter)
+ {
+ // Создаем новый контекст для альбома с указанным адаптером
+ return new PageContext
+ {
+ SessionKey = originalCtx.SessionKey,
+ Update = originalCtx.Update,
+ StateStorage = originalCtx.StateStorage,
+ Navigation = originalCtx.Navigation,
+ AdapterFactory = originalCtx.AdapterFactory,
+ Adapter = adapter,
+ };
+ }
+
+ ///
+ /// Сбросить состояние билдера для повторного использования.
+ ///
+ public MessageBuilder Reset()
+ {
+ _text = null;
+ _files.Clear();
+ _album.Clear();
+ _adapterOptions = null;
+ _targetAdapter = null;
+ _targetChatId = null;
+ _replyToMessageId = null;
+ _quoteReply = true;
+ _quoteTitle = null;
+ _disableWebPagePreview = false;
+ _disableNotification = false;
+ _protectContent = false;
+ _style = null;
+ _scheduleDate = null;
+ _topic = null;
+ _messageId = null;
+ _reply.Clear();
+ _inline.Clear();
+ _disableReplyKeyboard = false;
+
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/BotPages.Core/Messaging/MessageBuilderAdapterExtensions.cs b/BotPages.Core/Messaging/MessageBuilderAdapterExtensions.cs
new file mode 100644
index 0000000..4ea7030
--- /dev/null
+++ b/BotPages.Core/Messaging/MessageBuilderAdapterExtensions.cs
@@ -0,0 +1,55 @@
+using BotPages.Core.Abstractions;
+
+namespace BotPages.Core.Messaging;
+
+///
+/// Расширения для MessageBuilder для работы с разными адаптерами.
+///
+public static class MessageBuilderAdapterExtensions
+{
+ ///
+ /// Отправить сообщение через конкретный адаптер.
+ ///
+ public static MessageBuilder WithAdapter(this MessageBuilder builder, IMessengerAdapter adapter)
+ {
+ builder.WithAdapter(adapter);
+ return builder;
+ }
+
+ ///
+ /// Отправить сообщение через адаптер по типу и ID экземпляра.
+ ///
+ public static MessageBuilder WithAdapter(this MessageBuilder builder, string messengerType, string instanceId)
+ {
+ // Получаем фабрику адаптеров через сервис локатор или контекст
+ // В реальной реализации нужно будет передать фабрику адаптеров
+ return builder;
+ }
+
+ ///
+ /// Отправить сообщение через адаптер по полному ключу.
+ ///
+ public static MessageBuilder WithAdapter(this MessageBuilder builder, string fullMessengerKey)
+ {
+ // В реальной реализации нужно будет передать фабрику адаптеров
+ return builder;
+ }
+
+ ///
+ /// Отправить сообщение в указанный чат с указанным адаптером.
+ ///
+ public static MessageBuilder ToChat(this MessageBuilder builder, string chatId, IMessengerAdapter adapter)
+ {
+ return builder.ToChat(chatId, adapter);
+ }
+
+ ///
+ /// Отправить сообщение в чат в другом адаптере того же типа.
+ ///
+ public static MessageBuilder ToChatInOtherInstance(this MessageBuilder builder, string chatId, string instanceId)
+ {
+ // Получаем тип текущего адаптера и используем другой экземпляр
+ // В реальной реализации нужно будет передать фабрику адаптеров
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/BotPages.Core/Middleware/LoggingMiddleware.cs b/BotPages.Core/Middleware/LoggingMiddleware.cs
index f56cf5f..a01cb03 100644
--- a/BotPages.Core/Middleware/LoggingMiddleware.cs
+++ b/BotPages.Core/Middleware/LoggingMiddleware.cs
@@ -17,7 +17,7 @@ public sealed class LoggingMiddleware : IPageMiddleware
public async Task InvokeAsync(PageContext ctx, Func next, CancellationToken ct)
{
// Логируем базовую информацию
- _logger.Log(LogLevel.Info, $"Update from {ctx.Update.MessengerType} | Chat: {ctx.Update.Chat.Id} | User: {ctx.Update.User.Id}");
+ _logger.Log(LogLevel.Info, $"Update from {ctx.Update.AdapterId} | Chat: {ctx.Update.Chat.Id} | User: {ctx.Update.User.Id}");
// Логируем текст, кнопки, файлы
if (ctx.Update.Text is not null)
diff --git a/BotPages.Core/Pages/Page.cs b/BotPages.Core/Pages/Page.cs
index 82f4c46..cf279dd 100644
--- a/BotPages.Core/Pages/Page.cs
+++ b/BotPages.Core/Pages/Page.cs
@@ -10,19 +10,34 @@ public abstract class Page
{
/// Вход на страницу.
public virtual Task OnEnter(PageContext ctx, CancellationToken ct) => Task.CompletedTask;
+
/// Общий обработчик обновлений.
public virtual Task OnUpdate(PageContext ctx, UpdateContext update, CancellationToken ct) => Task.CompletedTask;
+
/// Обработка текста.
public virtual Task OnText(PageContext ctx, string text, CancellationToken ct) => Task.CompletedTask;
+
/// Обработка файлов.
public virtual Task OnFile(PageContext ctx, List files, CancellationToken ct) => Task.CompletedTask;
+
/// Обработка кнопки.
public virtual Task OnButton(PageContext ctx, string payload, CancellationToken ct) => Task.CompletedTask;
+
+ /// Обработка редактирования сообщения.
+ public virtual Task OnEdit(PageContext ctx, EditInfo editInfo, CancellationToken ct) => Task.CompletedTask;
+
+ /// Обработка удаления сообщения.
+ public virtual Task OnDelete(PageContext ctx, DeleteInfo deleteInfo, CancellationToken ct) => Task.CompletedTask;
+
+ /// Обработка закрепления сообщения.
+ public virtual Task OnPin(PageContext ctx, PinInfo pinInfo, CancellationToken ct) => Task.CompletedTask;
+
/// Выход со страницы.
public virtual Task OnLeave(PageContext ctx, CancellationToken ct) => Task.CompletedTask;
+
/// Таймаут бездействия.
public virtual Task OnTimeout(PageContext ctx, TimeSpan timeout, CancellationToken ct) => Task.CompletedTask;
+
/// Обработка ошибок.
public virtual Task OnError(PageContext ctx, Exception ex, CancellationToken ct) => Task.CompletedTask;
-
}
\ No newline at end of file
diff --git a/BotPages.Telegram/BotPagesAppExtension.cs b/BotPages.Telegram/BotPagesAppExtension.cs
index 2f9cb98..1c738a7 100644
--- a/BotPages.Telegram/BotPagesAppExtension.cs
+++ b/BotPages.Telegram/BotPagesAppExtension.cs
@@ -1,4 +1,5 @@
using BotPages.Core;
+using System;
namespace BotPages.Telegram;
@@ -8,25 +9,28 @@ namespace BotPages.Telegram;
public static class BotPagesAppExtension
{
///
- /// Добавление адаптера для телеграмм в
+ /// Добавление адаптера для Telegram.
///
- ///
- ///
- ///
- ///
- public static BotPagesApp AddTelegramAdapter(this BotPagesApp app, string token, string messengerType = "")
- => app.AddTelegramAdapter(token, null, messengerType);
-
- ///
- /// Добавление адаптера для телеграмм с опциями.
- ///
- public static BotPagesApp AddTelegramAdapter(this BotPagesApp app, string token, TelegramOptions? options, string messengerType = "")
+ /// Если адаптер с таким ID уже существует.
+ public static BotPagesApp AddTelegramAdapter(this BotPagesApp app, string token, string adapterId, TelegramOptions? options = null)
{
+ if (app.HasAdapter(adapterId))
+ {
+ throw new ArgumentException($"Adapter with ID '{adapterId}' already exists", nameof(adapterId));
+ }
+
var telegram = new TelegramAdapter(app.Logger, token, options);
-
- if (!string.IsNullOrWhiteSpace(messengerType)) telegram.MessengerType = messengerType;
-
- app.AddAdapter(telegram.MessengerType, telegram);
+ app.AddAdapter(adapterId, telegram);
return app;
}
-}
+
+ ///
+ /// Добавить Telegram бота с автоматическим ID.
+ ///
+ public static BotPagesApp AddTelegramAdapter(this BotPagesApp app, string token, TelegramOptions? options = null)
+ {
+ var telegram = new TelegramAdapter(app.Logger, token, options);
+ app.AddAdapter(telegram);
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/BotPages.Telegram/MessageBuilderExtensions.cs b/BotPages.Telegram/MessageBuilderExtensions.cs
deleted file mode 100644
index 0df0629..0000000
--- a/BotPages.Telegram/MessageBuilderExtensions.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using BotPages.Core.Messaging;
-
-namespace BotPages.Telegram;
-
-///
-/// `MessageBuilder` Telegram.
-/// `TelegramOptions`
-/// .
-///
-public static class MessageBuilderExtensions
-{
- ///
- /// Telegram ( ).
- ///
- public static MessageBuilder WithTelegramOptions(this MessageBuilder builder, TelegramOptions options)
- {
- return builder.WithAdapterOption(TelegramAdapter.AdapterType, options);
- }
-}
diff --git a/BotPages.Telegram/TelegramAdapter.cs b/BotPages.Telegram/TelegramAdapter.cs
index 6f65992..ea5ece3 100644
--- a/BotPages.Telegram/TelegramAdapter.cs
+++ b/BotPages.Telegram/TelegramAdapter.cs
@@ -16,15 +16,12 @@ using Telegram.Bot.Types.ReplyMarkups;
namespace BotPages.Telegram;
-
///
/// Адаптер для Telegram на базе Telegram.Bot.
/// Реализует отправку текста, кнопок, файлов, альбомов и прогресса.
///
-public sealed class TelegramAdapter : IMessengerAdapterSetup
+public sealed class TelegramAdapter : MessengerAdapterBase
{
- internal static readonly string AdapterType = typeof(TelegramAdapter).FullName;
-
private readonly ILogger _logger;
private TelegramBotClient? _client;
private string _token;
@@ -45,29 +42,35 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
_logger = logger;
_token = token;
_options = options ?? new TelegramOptions();
+
+ // Устанавливаем имя для отображения
+ DisplayName = $"Telegram Bot";
}
+ /// Тип адаптера.
+ public override string AdapterType => "Telegram";
+
///
- ///Идентификатор мессенджера / адаптера
+ /// Идентификатор мессенджера / адаптера
///
- public string MessengerType { get; set; } = "Telegram: " + Guid.NewGuid().ToString();
+ public string MessengerType => "Telegram";
///
/// Доступные возможности адаптера.
///
- public Capabilities Capabilities => _capabilities;
+ public override Capabilities Capabilities => _capabilities;
///
/// Запустить polling для приема обновлений от Telegram.
///
- public async Task StartAdapterAsync(Func onUpdate, List commands, CancellationToken ct)
+ public override async Task StartAdapterAsync(Func onUpdate, List commands, CancellationToken ct)
{
_client = new TelegramBotClient(_token);
_client.StartReceiving(
updateHandler: async (_, update, ct2) =>
{
- var mapped = TelegramUpdateMapper.Map(MessengerType, update, _client);
+ var mapped = TelegramUpdateMapper.Map(this, update, _client);
if (mapped is not null)
await onUpdate(mapped);
if (update.CallbackQuery is not null)
@@ -78,18 +81,18 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
errorHandler: async (_, ex, ct2) =>
{
- _logger.Log(LogLevel.Warn, $"{MessengerType} error.", ex);
+ _logger.Log(LogLevel.Warn, $"{AdapterType} ({AdapterId}) error.", ex);
await Task.CompletedTask;
},
cancellationToken: ct
);
-
await _client.SetMyCommands(commands.Where(t => t.Publish).Select(t => new BotCommand(t.Name, t.Description ?? t.Name.TrimStart('/'))), cancellationToken: ct);
var me = await _client.GetMe();
- _logger.Log(LogLevel.Info, $"{MessengerType} started: @{me.Username}");
+ DisplayName = $"Telegram: @{me.Username}";
+ _logger.Log(LogLevel.Info, $"{AdapterType} ({AdapterId}) started: {DisplayName}");
return;
}
@@ -98,7 +101,7 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
/// Универсальный внутренний метод отправки — определяет, нужно ли отправлять текст или файл по параметрам.
/// Возвращает id сообщения (или null).
///
- public async Task SendAsync(SendRequest req, CancellationToken ct)
+ public override async Task SendAsync(SendRequest req, CancellationToken ct)
{
if (_client is null)
{
@@ -112,33 +115,80 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
{
telegramOptions = opt;
}
- var disableNotification = !telegramOptions.NotifyOnSend;
+ var disableNotification = !telegramOptions.NotifyOnSend || req.DisableNotification;
// Build markup
var inlineMarkup = BuildInlineMarkup(req.Inline);
ReplyMarkup? markup = BuildReplyMarkup(req.Reply);
+ // Ответ на сообщение
+ int? replyToMessageId = null;
+ if (!string.IsNullOrEmpty(req.ReplyToMessageId) && int.TryParse(req.ReplyToMessageId, out var replyId))
+ {
+ replyToMessageId = replyId;
+ }
+
// Файлы: сейчас поддерживается один файл через SendRequest.File.
// При необходимости для нескольких файлов следует использовать альбомы (CreateAlbumBuilder)
if (req.File is not null)
{
if (inlineMarkup is not null) markup = inlineMarkup;
- var sent = await SendFileAsync(req.File, req.ChatId, markup, disableNotification, req.Caption, req.CaptionFormat, ct);
+ var sent = await SendFileAsync(req.File, req.ChatId, markup, disableNotification,
+ req.Caption, req.CaptionFormat, replyToMessageId, req.ProtectContent, ct);
return sent?.MessageId.ToString();
}
// Текст
if (!string.IsNullOrWhiteSpace(req.Text))
{
- return await SendTextAsync(req, inlineMarkup, markup, disableNotification, ct);
+ return await SendTextAsync(req, inlineMarkup, markup, disableNotification,
+ replyToMessageId, req.DisableWebPagePreview, req.ProtectContent, ct);
}
return null;
}
///
- public Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default)
+ public override Task DeleteAsync(string chatId, string messageId, CancellationToken ct = default)
+ {
+ if (_client is null)
+ {
+ _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized.");
+ return Task.CompletedTask;
+ }
+
+ return _client.DeleteMessage(chatId, Convert.ToInt32(messageId), ct);
+ }
+
+ ///
+ public override async Task DeleteMultipleAsync(string chatId, IEnumerable messageIds, CancellationToken ct = default)
+ {
+ if (_client is null)
+ {
+ _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized.");
+ return false;
+ }
+
+ try
+ {
+ // Telegram не поддерживает массовое удаление, удаляем по одному
+ foreach (var messageId in messageIds)
+ {
+ await DeleteAsync(chatId, messageId, ct);
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.Log(LogLevel.Warn, $"Failed to delete multiple messages: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ public override async Task EditTextAsync(string chatId, string messageId, string text,
+ MessageFormat? format = null, CancellationToken ct = default)
{
if (_client is null)
{
@@ -146,7 +196,171 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
return null;
}
- return _client.DeleteMessage(chatId, Convert.ToInt32(messageId), ct);
+ try
+ {
+ var parseMode = GetParseMode(format);
+ await _client.EditMessageText(
+ chatId: long.Parse(chatId),
+ messageId: int.Parse(messageId),
+ text: text,
+ parseMode: parseMode,
+ cancellationToken: ct
+ );
+ return messageId;
+ }
+ catch (Exception ex)
+ {
+ _logger.Log(LogLevel.Warn, $"Failed to edit text: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ public override async Task EditButtonsAsync(string chatId, string messageId,
+ IEnumerable>? inlineButtons = null, CancellationToken ct = default)
+ {
+ if (_client is null)
+ {
+ _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized.");
+ return null;
+ }
+
+ try
+ {
+ var inlineMarkup = BuildInlineMarkup(inlineButtons);
+ await _client.EditMessageReplyMarkup(
+ chatId: long.Parse(chatId),
+ messageId: int.Parse(messageId),
+ replyMarkup: inlineMarkup,
+ cancellationToken: ct
+ );
+ return messageId;
+ }
+ catch (Exception ex)
+ {
+ _logger.Log(LogLevel.Warn, $"Failed to edit buttons: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ public override async Task PinMessageAsync(string chatId, string messageId, bool disableNotification = false,
+ CancellationToken ct = default)
+ {
+ if (_client is null)
+ {
+ _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized.");
+ return false;
+ }
+
+ try
+ {
+ await _client.PinChatMessage(
+ chatId: long.Parse(chatId),
+ messageId: int.Parse(messageId),
+ disableNotification: disableNotification,
+ cancellationToken: ct
+ );
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.Log(LogLevel.Warn, $"Failed to pin message: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ public override async Task UnpinMessageAsync(string chatId, string messageId, CancellationToken ct = default)
+ {
+ if (_client is null)
+ {
+ _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized.");
+ return false;
+ }
+
+ try
+ {
+ await _client.UnpinChatMessage(
+ chatId: long.Parse(chatId),
+ messageId: int.Parse(messageId),
+ cancellationToken: ct
+ );
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.Log(LogLevel.Warn, $"Failed to unpin message: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ public override async Task GetMessageInfoAsync(string chatId, string messageId, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public override async Task ForwardMessageAsync(string fromChatId, string messageId, string toChatId,
+ bool disableNotification = false, CancellationToken ct = default)
+ {
+ if (_client is null)
+ {
+ _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized.");
+ return null;
+ }
+
+ try
+ {
+ var message = await _client.ForwardMessage(
+ chatId: long.Parse(toChatId),
+ fromChatId: long.Parse(fromChatId),
+ messageId: int.Parse(messageId),
+ disableNotification: disableNotification,
+ cancellationToken: ct
+ );
+
+ return message.MessageId.ToString();
+ }
+ catch (Exception ex)
+ {
+ _logger.Log(LogLevel.Warn, $"Failed to forward message: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ public override async Task CopyMessageAsync(string fromChatId, string messageId, string toChatId,
+ string? caption = null, MessageFormat? captionFormat = null,
+ bool disableNotification = false, CancellationToken ct = default)
+ {
+ if (_client is null)
+ {
+ _logger.Log(LogLevel.Critical, $"{MessengerType} client is not initialized.");
+ return null;
+ }
+
+ try
+ {
+ var parseMode = GetParseMode(captionFormat);
+ var message = await _client.CopyMessage(
+ chatId: long.Parse(toChatId),
+ fromChatId: long.Parse(fromChatId),
+ messageId: int.Parse(messageId),
+ caption: caption,
+ parseMode: parseMode,
+ disableNotification: disableNotification,
+ cancellationToken: ct
+ );
+
+ return message.Id.ToString();
+ }
+ catch (Exception ex)
+ {
+ _logger.Log(LogLevel.Warn, $"Failed to copy message: {ex.Message}");
+ return null;
+ }
}
// --- Helpers ---
@@ -209,21 +423,64 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
}
}
- private async Task SendFileAsync(FileDescriptor file, string chatId, ReplyMarkup? markup, bool disableNotification, string? caption, MessageFormat? captionFormat, CancellationToken ct)
+ private async Task SendFileAsync(FileDescriptor file, string chatId, ReplyMarkup? markup,
+ bool disableNotification, string? caption, MessageFormat? captionFormat, int? replyToMessageId,
+ bool protectContent, CancellationToken ct)
{
var inputFile = await CreateInputFileAsync(file, ct);
var parseMode = GetParseMode(captionFormat);
return file.Kind switch
{
- FileKind.Photo => await _client.SendPhoto(long.Parse(chatId), inputFile, caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct),
- FileKind.Video => await _client.SendVideo(long.Parse(chatId), inputFile, caption: caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct),
- FileKind.Audio => await _client.SendAudio(long.Parse(chatId), inputFile, caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct),
- _ => await _client.SendDocument(long.Parse(chatId), inputFile, caption ?? "", parseMode, replyMarkup: markup, disableNotification: disableNotification, cancellationToken: ct),
+ FileKind.Photo => await _client.SendPhoto(
+ long.Parse(chatId),
+ inputFile,
+ caption ?? "",
+ parseMode,
+ replyParameters: replyToMessageId,
+ replyMarkup: markup,
+ disableNotification: disableNotification,
+ protectContent: protectContent,
+ cancellationToken: ct),
+
+ FileKind.Video => await _client.SendVideo(
+ long.Parse(chatId),
+ inputFile,
+ caption: caption ?? "",
+ parseMode,
+ replyParameters: replyToMessageId,
+ replyMarkup: markup,
+ disableNotification: disableNotification,
+ protectContent: protectContent,
+ cancellationToken: ct),
+
+ FileKind.Audio => await _client.SendAudio(
+ long.Parse(chatId),
+ inputFile,
+ caption ?? "",
+ parseMode,
+ replyParameters: replyToMessageId,
+ replyMarkup: markup,
+ disableNotification: disableNotification,
+ protectContent: protectContent,
+ cancellationToken: ct),
+
+ _ => await _client.SendDocument(
+ long.Parse(chatId),
+ inputFile,
+ caption ?? "",
+ parseMode,
+ replyParameters: replyToMessageId,
+ replyMarkup: markup,
+ disableNotification: disableNotification,
+ protectContent: protectContent,
+ cancellationToken: ct),
};
}
- private async Task SendTextAsync(SendRequest req, InlineKeyboardMarkup? inlineMarkup, ReplyMarkup? markup, bool disableNotification, CancellationToken ct)
+ private async Task SendTextAsync(SendRequest req, InlineKeyboardMarkup? inlineMarkup,
+ ReplyMarkup? markup, bool disableNotification, int? replyToMessageId,
+ bool disableWebPagePreview, bool protectContent, CancellationToken ct)
{
var format = req.TextFormat ?? MessageFormat.Plain;
var parseMode = GetParseMode(format);
@@ -242,6 +499,10 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
chatId: long.Parse(req.ChatId),
text: text,
parseMode: parseMode,
+ linkPreviewOptions: new()
+ {
+ IsDisabled = disableWebPagePreview,
+ },
replyMarkup: inlineMarkup,
cancellationToken: ct
);
@@ -256,8 +517,14 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
chatId: long.Parse(req.ChatId),
text: text,
parseMode: parseMode,
+ linkPreviewOptions: new()
+ {
+ IsDisabled = disableWebPagePreview,
+ },
replyMarkup: markup,
+ replyParameters: replyToMessageId,
disableNotification: disableNotification,
+ protectContent: protectContent,
cancellationToken: ct
);
@@ -266,8 +533,8 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
}
///
- public IAlbumBuilder CreateAlbumBuilder(PageContext ctx) => new TelegramAlbumBuilder(this, ctx, _logger, _client);
+ public override IAlbumBuilder CreateAlbumBuilder(PageContext ctx) => new TelegramAlbumBuilder(this, ctx, _logger, _client);
///
- public Task OnLeaveAsync(PageContext ctx, CancellationToken ct) => Task.CompletedTask;
+ public override Task OnLeaveAsync(PageContext ctx, CancellationToken ct) => Task.CompletedTask;
}
\ No newline at end of file
diff --git a/BotPages.Telegram/TelegramUpdateMapper.cs b/BotPages.Telegram/TelegramUpdateMapper.cs
index 8c5728e..c17ae65 100644
--- a/BotPages.Telegram/TelegramUpdateMapper.cs
+++ b/BotPages.Telegram/TelegramUpdateMapper.cs
@@ -19,29 +19,56 @@ public static class TelegramUpdateMapper
///
/// Маппинг Telegram Update в UpdateContext BotPages.
///
- public static UpdateContext Map(string MessengerType, Update update, TelegramBotClient client)
+ public static UpdateContext? Map(TelegramAdapter adapter, Update update, TelegramBotClient client)
{
- var chat = update.Message?.Chat ?? update.CallbackQuery?.Message?.Chat;
- var user = update.Message?.From ?? update.CallbackQuery?.From;
+ var chat = update.Message?.Chat ?? update.CallbackQuery?.Message?.Chat ?? update.EditedMessage?.Chat;
+ var user = update.Message?.From ?? update.CallbackQuery?.From ?? update.EditedMessage?.From;
+
+ if (chat == null || user == null)
+ return null;
var userContext = new UserContext
{
- Id = user?.Id.ToString() ?? "unknown",
- DisplayName = user?.Username,
+ Id = user.Id.ToString(),
+ DisplayName = user.Username,
};
var chatContext = new ChatContext
{
- Id = chat?.Id.ToString() ?? "unknown",
- Title = chat?.Title,
+ Id = chat.Id.ToString(),
+ Title = chat.Title,
};
string? text = null;
UpdateKind kind = UpdateKind.None;
var files = new List();
+ string? messageId = null;
+ string? replyToMessageId = null;
+ EditInfo? editInfo = null;
+ DeleteInfo? deleteInfo = null;
+ PinInfo? pinInfo = null;
+ // Обработка разных типов обновлений
if (update.Message is { } msg)
{
+ messageId = msg.MessageId.ToString();
+
+ if (msg.ReplyToMessage != null)
+ {
+ kind |= UpdateKind.Reply;
+ replyToMessageId = msg.ReplyToMessage.MessageId.ToString();
+ }
+
+ if (msg.PinnedMessage != null)
+ {
+ kind |= UpdateKind.Pin;
+ pinInfo = new PinInfo
+ {
+ MessageId = msg.PinnedMessage.MessageId.ToString(),
+ PinDate = msg.Date,
+ NotificationDisabled = false // Telegram не передает эту информацию
+ };
+ }
if (msg.Text is not null)
{
@@ -112,28 +139,66 @@ public static class TelegramUpdateMapper
kind |= UpdateKind.File;
}
}
-
- if (update.CallbackQuery is { } cb)
+ else if (update.EditedMessage is { } editedMsg)
{
+ messageId = editedMsg.MessageId.ToString();
+ kind |= UpdateKind.Edit;
+ text = editedMsg.Text;
+
+ editInfo = new EditInfo
+ {
+ NewText = editedMsg.Text,
+ EditDate = editedMsg.EditDate ?? DateTime.UtcNow
+ };
+ }
+ else if (update.CallbackQuery is { } cb)
+ {
+ messageId = cb.Message?.MessageId.ToString();
kind |= UpdateKind.Button;
text = cb.Data;
}
+ else if (update.ChannelPost is { } channelPost)
+ {
+ messageId = channelPost.MessageId.ToString();
+ if (channelPost.Text is not null)
+ {
+ text = channelPost.Text;
+ kind |= UpdateKind.Text;
+ }
+ }
+ else if (update.EditedChannelPost is { } editedChannelPost)
+ {
+ messageId = editedChannelPost.MessageId.ToString();
+ kind |= UpdateKind.Edit;
+ text = editedChannelPost.Text;
+
+ editInfo = new EditInfo
+ {
+ NewText = editedChannelPost.Text,
+ EditDate = editedChannelPost.EditDate ?? DateTime.UtcNow
+ };
+ }
return new UpdateContext
{
- MessengerType = MessengerType,
+ AdapterId = adapter.AdapterId,
+ AdapterType = adapter.AdapterType,
User = userContext,
Chat = chatContext,
Text = text,
Kind = kind,
- Files = files
+ MessageId = messageId,
+ ReplyToMessageId = replyToMessageId,
+ Files = files,
+ EditInfo = editInfo,
+ DeleteInfo = deleteInfo,
+ PinInfo = pinInfo
};
}
private static Func> GetStreamAsync(TelegramBotClient client, string fileId)
{
-
Func> getStreamAsync = async _ =>
{
var file = await client.GetFile(fileId);
diff --git a/Demo/Program.cs b/Demo/Program.cs
index fb95aae..b54d23f 100644
--- a/Demo/Program.cs
+++ b/Demo/Program.cs
@@ -51,8 +51,16 @@ namespace Demo
.AddTelegramAdapter(token, "Telegram")
.Build(cts.Token);
- Console.ReadKey();
- cts.Cancel();
+
+ Console.WriteLine("Bot is running. Press Ctrl+C to stop.");
+ Console.CancelKeyPress += (sender, e) =>
+ {
+ Console.WriteLine("Cancel key pressed");
+ cts.Cancel();
+ e.Cancel = true;
+ };
+
+ app.Wait();
}
}
}