Добавьте файлы проекта.
This commit is contained in:
9
BotPages.Core/BotPages.Core.csproj
Normal file
9
BotPages.Core/BotPages.Core.csproj
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
18
BotPages.Core/Context/ChatContext.cs
Normal file
18
BotPages.Core/Context/ChatContext.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Описывает чат/конверсацию для универсального контекста.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChatContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Уникальный идентификатор чата/диалога.
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Человеко-читаемое имя чата (если доступно).
|
||||||
|
/// </summary>
|
||||||
|
public string? Title { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
BotPages.Core/Context/FileDescriptor.cs
Normal file
12
BotPages.Core/Context/FileDescriptor.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Универсальный дескриптор файла для операций загрузки/отправки.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FileDescriptor(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
string MimeType,
|
||||||
|
Stream? Content = null
|
||||||
|
);
|
||||||
|
}
|
||||||
48
BotPages.Core/Context/UpdateContext.cs
Normal file
48
BotPages.Core/Context/UpdateContext.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Универсальный контекст обновления, независимый от транспорта.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Клиент транспорта для отправки сообщений/файлов.
|
||||||
|
/// </summary>
|
||||||
|
public required IChatClient Client { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Контекст чата.
|
||||||
|
/// </summary>
|
||||||
|
public required ChatContext Chat { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Контекст пользователя.
|
||||||
|
/// </summary>
|
||||||
|
public required UserContext User { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Текст сообщения или полезная нагрузка колбэка, если доступна.
|
||||||
|
/// </summary>
|
||||||
|
public string? Text { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Список полученных файлов (если транспорт поддерживает).
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<FileDescriptor>? IncomingFiles { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сырой объект обновления транспорта (например, Telegram.Update).
|
||||||
|
/// </summary>
|
||||||
|
public object? RawUpdate { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сервис навигации страниц.
|
||||||
|
/// </summary>
|
||||||
|
public required INavigationService Nav { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Хранилище состояния пользователя.
|
||||||
|
/// </summary>
|
||||||
|
public required IStateStore State { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
18
BotPages.Core/Context/UserContext.cs
Normal file
18
BotPages.Core/Context/UserContext.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Описывает пользователя для универсального контекста.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UserContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Уникальный идентификатор пользователя в транспортном слое.
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отображаемое имя пользователя (если доступно).
|
||||||
|
/// </summary>
|
||||||
|
public string? DisplayName { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
38
BotPages.Core/Navigation/INavigationService.cs
Normal file
38
BotPages.Core/Navigation/INavigationService.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Сервис навигации по страницам.
|
||||||
|
/// </summary>
|
||||||
|
public interface INavigationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет push новой страницы и вызывает её Enter.
|
||||||
|
/// </summary>
|
||||||
|
Task PushAsync(string pageId, object? args, UpdateContext ctx, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет replace текущей страницы и вызывает Enter новой.
|
||||||
|
/// </summary>
|
||||||
|
Task ReplaceAsync(string pageId, object? args, UpdateContext ctx, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращается назад по стеку и вызывает Enter предыдущей.
|
||||||
|
/// </summary>
|
||||||
|
Task PopAsync(UpdateContext ctx, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Применяет декларативный результат страницы (навигация, текст, файлы).
|
||||||
|
/// </summary>
|
||||||
|
Task ApplyResultAsync(UpdateContext ctx, PageResult result, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает текущую запись стека.
|
||||||
|
/// </summary>
|
||||||
|
Task<NavEntry?> CurrentAsync(UpdateContext ctx, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает весь стек навигации.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<NavEntry>> StackAsync(UpdateContext ctx, CancellationToken ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
BotPages.Core/Navigation/IStateStore.cs
Normal file
18
BotPages.Core/Navigation/IStateStore.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Простое in-memory хранилище состояния пользователя.
|
||||||
|
/// </summary>
|
||||||
|
public interface IStateStore
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает состояние пользователя.
|
||||||
|
/// </summary>
|
||||||
|
Task<UserState> GetAsync(long userId, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сохраняет состояние пользователя.
|
||||||
|
/// </summary>
|
||||||
|
Task SaveAsync(UserState state, CancellationToken ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
BotPages.Core/Navigation/InMemoryStateStore.cs
Normal file
33
BotPages.Core/Navigation/InMemoryStateStore.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-memory реализация хранилища состояния для прототипирования.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InMemoryStateStore : IStateStore
|
||||||
|
{
|
||||||
|
private readonly Dictionary<long, UserState> _store = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает состояние пользователя, создавая новое при отсутствии.
|
||||||
|
/// </summary>
|
||||||
|
public Task<UserState> GetAsync(long userId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!_store.TryGetValue(userId, out var st))
|
||||||
|
{
|
||||||
|
st = new UserState { UserId = userId };
|
||||||
|
_store[userId] = st;
|
||||||
|
}
|
||||||
|
return Task.FromResult(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сохраняет состояние пользователя.
|
||||||
|
/// </summary>
|
||||||
|
public Task SaveAsync(UserState state, CancellationToken ct)
|
||||||
|
{
|
||||||
|
_store[state.UserId] = state;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
BotPages.Core/Navigation/NavigationService.cs
Normal file
108
BotPages.Core/Navigation/NavigationService.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Реализация сервиса навигации страниц.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NavigationService : INavigationService
|
||||||
|
{
|
||||||
|
private readonly IPageRegistry _pages;
|
||||||
|
private readonly IStateStore _store;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт сервис навигации.
|
||||||
|
/// </summary>
|
||||||
|
public NavigationService(IPageRegistry pages, IStateStore store)
|
||||||
|
{
|
||||||
|
_pages = pages;
|
||||||
|
_store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет push новой страницы и вызывает её Enter.
|
||||||
|
/// </summary>
|
||||||
|
public async Task PushAsync(string pageId, object? args, UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var state = await _store.GetAsync(ctx.User.Id, ct);
|
||||||
|
state.Stack.Add(new NavEntry(pageId, args));
|
||||||
|
await _store.SaveAsync(state, ct);
|
||||||
|
|
||||||
|
var pr = await _pages.Get(pageId).EnterAsync(ctx, ct);
|
||||||
|
await ApplyResultAsync(ctx, pr, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет replace текущей страницы и вызывает Enter новой.
|
||||||
|
/// </summary>
|
||||||
|
public async Task ReplaceAsync(string pageId, object? args, UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var state = await _store.GetAsync(ctx.User.Id, ct);
|
||||||
|
if (state.Stack.Count > 0) state.Stack[^1] = new NavEntry(pageId, args);
|
||||||
|
else state.Stack.Add(new NavEntry(pageId, args));
|
||||||
|
await _store.SaveAsync(state, ct);
|
||||||
|
|
||||||
|
var pr = await _pages.Get(pageId).EnterAsync(ctx, ct);
|
||||||
|
await ApplyResultAsync(ctx, pr, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращается назад по стеку и вызывает Enter предыдущей.
|
||||||
|
/// </summary>
|
||||||
|
public async Task PopAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var state = await _store.GetAsync(ctx.User.Id, ct);
|
||||||
|
if (state.Stack.Count == 0) return;
|
||||||
|
|
||||||
|
var currentId = state.Stack[^1].PageId;
|
||||||
|
await _pages.Get(currentId).ExitAsync(ctx, ct);
|
||||||
|
|
||||||
|
state.Stack.RemoveAt(state.Stack.Count - 1);
|
||||||
|
await _store.SaveAsync(state, ct);
|
||||||
|
|
||||||
|
var next = state.Stack.Count > 0 ? state.Stack[^1].PageId : null;
|
||||||
|
if (next is not null)
|
||||||
|
{
|
||||||
|
var pr = await _pages.Get(next).EnterAsync(ctx, ct);
|
||||||
|
await ApplyResultAsync(ctx, pr, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Применяет декларативный результат страницы (навигация, текст, файлы).
|
||||||
|
/// </summary>
|
||||||
|
public async Task ApplyResultAsync(UpdateContext ctx, PageResult result, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (result.NavigateTo is not null)
|
||||||
|
{
|
||||||
|
if (result.NavigateTo.Replace)
|
||||||
|
await ReplaceAsync(result.NavigateTo.PageId, result.NavigateTo.Args, ctx, ct);
|
||||||
|
else
|
||||||
|
await PushAsync(result.NavigateTo.PageId, result.NavigateTo.Args, ctx, ct);
|
||||||
|
return; // навигация сама вызовет Enter новой страницы и применит её результат
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Message is not null)
|
||||||
|
await ctx.Client.SendTextAsync(ctx.Chat.Id, result.Message, result.Actions, ct);
|
||||||
|
|
||||||
|
if (result.Files is not null)
|
||||||
|
await ctx.Client.SendFilesAsync(ctx.Chat.Id, result.Files, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает текущую запись стека.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<NavEntry?> CurrentAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var state = await _store.GetAsync(ctx.User.Id, ct);
|
||||||
|
return state.Stack.Count == 0 ? null : state.Stack[^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает весь стек навигации.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<NavEntry>> StackAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var state = await _store.GetAsync(ctx.User.Id, ct);
|
||||||
|
return state.Stack.AsReadOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
BotPages.Core/Navigation/UserState.cs
Normal file
23
BotPages.Core/Navigation/UserState.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Состояние пользователя: навигационный стек и общий словарь данных.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UserState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Идентификатор пользователя.
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Навигационный стек страниц.
|
||||||
|
/// </summary>
|
||||||
|
public List<NavEntry> Stack { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Общая сумка данных, доступная на всех страницах.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object?> Bag { get; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
BotPages.Core/Pages/ActionPlacement.cs
Normal file
20
BotPages.Core/Pages/ActionPlacement.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Тип размещения кнопки.
|
||||||
|
/// </summary>
|
||||||
|
public enum ActionPlacement
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Inline‑кнопка (под сообщением).
|
||||||
|
/// </summary>
|
||||||
|
Inline,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reply‑кнопка (заменяет системную клавиатуру).
|
||||||
|
/// </summary>
|
||||||
|
Reply
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
28
BotPages.Core/Pages/IPage.cs
Normal file
28
BotPages.Core/Pages/IPage.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Контракт страницы: экран диалога с жизненным циклом.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Статический идентификатор страницы.
|
||||||
|
/// </summary>
|
||||||
|
string Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при входе на страницу (рендер, приветствие).
|
||||||
|
/// </summary>
|
||||||
|
Task<PageResult> EnterAsync(UpdateContext ctx, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обработка входящего события/сообщения на странице.
|
||||||
|
/// </summary>
|
||||||
|
Task<PageResult> HandleAsync(UpdateContext ctx, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при выходе со страницы (очистка, финализация).
|
||||||
|
/// </summary>
|
||||||
|
Task ExitAsync(UpdateContext ctx, CancellationToken ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
BotPages.Core/Pages/IPageRegistry.cs
Normal file
26
BotPages.Core/Pages/IPageRegistry.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Реестр страниц с доступом по идентификатору.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPageRegistry
|
||||||
|
{
|
||||||
|
IPage DefaultPage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает страницу по идентификатору.
|
||||||
|
/// </summary>
|
||||||
|
IPage Get(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пытается получить страницу по идентификатору.
|
||||||
|
/// </summary>
|
||||||
|
bool TryGet(string id, out IPage? page);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает все зарегистрированные страницы.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<IPage> All();
|
||||||
|
IPage GetOrDefault(string id);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
BotPages.Core/Pages/NavEntry.cs
Normal file
7
BotPages.Core/Pages/NavEntry.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Запись навигационного стека: страница и её аргументы.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record NavEntry(string PageId, object? Args = null);
|
||||||
|
}
|
||||||
32
BotPages.Core/Pages/Page.cs
Normal file
32
BotPages.Core/Pages/Page.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Базовая реализация страницы без обязательных переопределений.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class Page : IPage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Идентификатор страницы.
|
||||||
|
/// </summary>
|
||||||
|
public virtual string Id => GetType().Name;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Виртуальный метод входа; по умолчанию ничего не делает.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Task<PageResult> EnterAsync(UpdateContext ctx, CancellationToken ct) =>
|
||||||
|
Task.FromResult(new PageResult());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Абстрактная обработка событий; обязателен к реализации.
|
||||||
|
/// </summary>
|
||||||
|
public abstract Task<PageResult> HandleAsync(UpdateContext ctx, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Виртуальный метод выхода; по умолчанию ничего не делает.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Task ExitAsync(UpdateContext ctx, CancellationToken ct) =>
|
||||||
|
Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
28
BotPages.Core/Pages/PageAction.cs
Normal file
28
BotPages.Core/Pages/PageAction.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Универсальное действие (кнопка), которое может быть отображено в разных клиентах.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PageAction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Текст кнопки, отображаемый пользователю.
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Значение (payload), которое будет передано в <see cref="UpdateContext.Text"/> при нажатии.
|
||||||
|
/// </summary>
|
||||||
|
public string Value { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тип кнопки: inline или reply.
|
||||||
|
/// </summary>
|
||||||
|
public ActionPlacement Placement { get; init; } = ActionPlacement.Inline;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Номер ряда для макета (0 — первая строка).
|
||||||
|
/// </summary>
|
||||||
|
public int Row { get; init; } = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
BotPages.Core/Pages/PageMessage.cs
Normal file
39
BotPages.Core/Pages/PageMessage.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Параметры сообщения.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PageMessage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Текст сообщения.
|
||||||
|
/// </summary>
|
||||||
|
public required string Text { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Формат сообщения (Plain/Markdown/Html).
|
||||||
|
/// </summary>
|
||||||
|
public MessageFormat Format { get; init; } = MessageFormat.Plain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправить сообщение без уведомления (тихий режим).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSilent { get; init; } = false;
|
||||||
|
|
||||||
|
public static implicit operator PageMessage(string text)
|
||||||
|
=> new PageMessage { Text = text, Format = MessageFormat.Plain };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тип форматирования сообщения.
|
||||||
|
/// </summary>
|
||||||
|
public enum MessageFormat
|
||||||
|
{
|
||||||
|
/// <summary>Обычный текст без форматирования.</summary>
|
||||||
|
Plain,
|
||||||
|
|
||||||
|
/// <summary>Markdown.</summary>
|
||||||
|
Markdown,
|
||||||
|
|
||||||
|
/// <summary>HTML.</summary>
|
||||||
|
Html,
|
||||||
|
}
|
||||||
21
BotPages.Core/Pages/PageNavigate.cs
Normal file
21
BotPages.Core/Pages/PageNavigate.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Параметры навигации на другую страницу.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PageNavigate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Идентификатор страницы, на которую нужно перейти.
|
||||||
|
/// </summary>
|
||||||
|
public required string PageId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Дополнительные аргументы для навигации.
|
||||||
|
/// </summary>
|
||||||
|
public object? Args { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заменить текущую навигацию на новую.
|
||||||
|
/// </summary>
|
||||||
|
public bool Replace { get; init; }
|
||||||
|
}
|
||||||
84
BotPages.Core/Pages/PageRegistry.cs
Normal file
84
BotPages.Core/Pages/PageRegistry.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Базовая реализация реестра страниц на словаре.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PageRegistry : IPageRegistry
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, IPage> _pages = new(StringComparer.Ordinal);
|
||||||
|
private readonly IPage _defaultPage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт реестр из набора страниц.
|
||||||
|
/// </summary>
|
||||||
|
public PageRegistry(IEnumerable<IPage> pages) : this(pages, pages.First())
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт реестр из набора страниц.
|
||||||
|
/// </summary>
|
||||||
|
public PageRegistry(IEnumerable<IPage> pages, IPage defaultPage)
|
||||||
|
{
|
||||||
|
foreach (var p in pages) _pages[p.Id] = p;
|
||||||
|
_defaultPage = defaultPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает страницу по идентификатору.
|
||||||
|
/// </summary>
|
||||||
|
public IPage Get(string id) => _pages[id];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает страницу по идентификатору. Если страницы нет, возвращает дефолтную.
|
||||||
|
/// </summary>
|
||||||
|
public IPage GetOrDefault(string id)
|
||||||
|
=> _pages.TryGetValue(id, out var page) ? page : _defaultPage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает дефолтную страницу.
|
||||||
|
/// </summary>
|
||||||
|
public IPage DefaultPage => _defaultPage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пытается получить страницу по идентификатору.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryGet(string id, out IPage? page) => _pages.TryGetValue(id, out page);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает все зарегистрированные страницы.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<IPage> All() => _pages.Values;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт реестр страниц из всех сборок приложения.
|
||||||
|
/// </summary>
|
||||||
|
public static PageRegistry CreateFromApplication(string? defaultPageId = null)
|
||||||
|
{
|
||||||
|
// Берём все загруженные сборки в текущем AppDomain
|
||||||
|
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||||
|
|
||||||
|
// Находим все классы, реализующие IPage
|
||||||
|
var pages = assemblies
|
||||||
|
.SelectMany(a => a.GetTypes())
|
||||||
|
.Where(t => typeof(IPage).IsAssignableFrom(t) && !t.IsAbstract && t.GetConstructor(Type.EmptyTypes) != null)
|
||||||
|
.Select(t => (IPage)Activator.CreateInstance(t)!)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (pages.Count == 0)
|
||||||
|
throw new InvalidOperationException($"В приложении не найдено ни одной страницы ({nameof(IPage)}).");
|
||||||
|
|
||||||
|
// Определяем страницу по умолчанию
|
||||||
|
var defaultPage = defaultPageId != null
|
||||||
|
? pages.FirstOrDefault(p => p.Id == defaultPageId)
|
||||||
|
: pages.First();
|
||||||
|
|
||||||
|
if (defaultPage == null)
|
||||||
|
throw new InvalidOperationException($"Не найдена страница с Id={defaultPageId}.");
|
||||||
|
|
||||||
|
return new PageRegistry(pages, defaultPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
28
BotPages.Core/Pages/PageResult.cs
Normal file
28
BotPages.Core/Pages/PageResult.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Результат обработки страницы: текст, файлы, кнопки или навигация.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PageResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Параметры перехода страницы, на которую нужно перейти.
|
||||||
|
/// </summary>
|
||||||
|
public PageNavigate? NavigateTo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Текст сообщения (опционально).
|
||||||
|
/// </summary>
|
||||||
|
public PageMessage? Message { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Файлы для отправки (опционально).
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<FileDescriptor>? Files { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кнопки (inline или reply), которые должны быть отображены пользователю.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PageAction>? Actions { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
98
BotPages.Core/Pages/PageResultBuilder.cs
Normal file
98
BotPages.Core/Pages/PageResultBuilder.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Билдер для удобного создания <see cref="PageResult"/>.
|
||||||
|
/// Мутабельный, но итоговый объект иммутабелен.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PageResultBuilder
|
||||||
|
{
|
||||||
|
private PageNavigate? _navigateTo;
|
||||||
|
private PageMessage? _message;
|
||||||
|
private List<FileDescriptor>? _files;
|
||||||
|
private List<PageAction>? _actions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Устанавливает текст сообщения.
|
||||||
|
/// </summary>
|
||||||
|
public PageResultBuilder WithText(string text, MessageFormat format)
|
||||||
|
=> WithText(new PageMessage()
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Format = format,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Устанавливает текст сообщения.
|
||||||
|
/// </summary>
|
||||||
|
public PageResultBuilder WithText(string text)
|
||||||
|
=> WithText(new PageMessage()
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Format = MessageFormat.Plain,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Устанавливает текст сообщения.
|
||||||
|
/// </summary>
|
||||||
|
public PageResultBuilder WithText(PageMessage message)
|
||||||
|
{
|
||||||
|
_message = message;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавляет клавиатуру (набор кнопок).
|
||||||
|
/// </summary>
|
||||||
|
public PageResultBuilder WithKeyboard(IEnumerable<PageAction> actions)
|
||||||
|
{
|
||||||
|
_actions = actions?.ToList();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавляет файлы.
|
||||||
|
/// </summary>
|
||||||
|
public PageResultBuilder WithFiles(IEnumerable<FileDescriptor> files)
|
||||||
|
{
|
||||||
|
_files = files?.ToList();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Устанавливает навигацию на другую страницу.
|
||||||
|
/// </summary>
|
||||||
|
public PageResultBuilder WithNavigate(string pageId, object? args = null, bool replace = true)
|
||||||
|
=> WithNavigate(new PageNavigate()
|
||||||
|
{
|
||||||
|
PageId = pageId,
|
||||||
|
Args = args,
|
||||||
|
Replace = replace,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Устанавливает навигацию на другую страницу.
|
||||||
|
/// </summary>
|
||||||
|
public PageResultBuilder WithNavigate(PageNavigate navigate)
|
||||||
|
{
|
||||||
|
_navigateTo = navigate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Собирает итоговый иммутабельный <see cref="PageResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
public PageResult Build() => new PageResult
|
||||||
|
{
|
||||||
|
Message = _message,
|
||||||
|
Actions = _actions,
|
||||||
|
Files = _files,
|
||||||
|
NavigateTo = _navigateTo,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт новый пустой билдер.
|
||||||
|
/// </summary>
|
||||||
|
public static PageResultBuilder Empty() => new PageResultBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
BotPages.Core/Pipeline/ErrorHandlingMiddleware.cs
Normal file
24
BotPages.Core/Pipeline/ErrorHandlingMiddleware.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware обработки ошибок для надёжности.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ErrorHandlingMiddleware : IUpdateMiddleware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Перехватывает исключения и отправляет сообщение об ошибке пользователю.
|
||||||
|
/// </summary>
|
||||||
|
public async Task InvokeAsync(UpdateContext ctx, Func<Task> next, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error: {ex}");
|
||||||
|
await ctx.Client.SendTextAsync(ctx.Chat.Id, "Произошла ошибка. Попробуйте ещё раз. /start", null, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
BotPages.Core/Pipeline/IRouter.cs
Normal file
13
BotPages.Core/Pipeline/IRouter.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Маршрутизатор обновлений на страницы.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRouter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет текущую страницу и вызывает её обработчик.
|
||||||
|
/// </summary>
|
||||||
|
Task RouteAsync(UpdateContext ctx, CancellationToken ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
BotPages.Core/Pipeline/IUpdateMiddleware.cs
Normal file
13
BotPages.Core/Pipeline/IUpdateMiddleware.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware обработки входящих обновлений.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUpdateMiddleware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывает промежуточную логику, затем следующий обработчик или роутер.
|
||||||
|
/// </summary>
|
||||||
|
Task InvokeAsync(UpdateContext ctx, Func<Task> next, CancellationToken ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
BotPages.Core/Pipeline/LoggingMiddleware.cs
Normal file
17
BotPages.Core/Pipeline/LoggingMiddleware.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware логирования входящих обновлений.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LoggingMiddleware : IUpdateMiddleware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Логирует базовую информацию об обновлении и вызывает следующий этап.
|
||||||
|
/// </summary>
|
||||||
|
public async Task InvokeAsync(UpdateContext ctx, Func<Task> next, CancellationToken ct)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[{DateTime.UtcNow:O}] Update: chat={ctx.Chat.Id}, user={ctx.User.Id}, text={ctx.Text}");
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
BotPages.Core/Pipeline/Pipeline.cs
Normal file
34
BotPages.Core/Pipeline/Pipeline.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Конвейер выполнения middleware и роутера.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Pipeline
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyList<IUpdateMiddleware> _middlewares;
|
||||||
|
private readonly IRouter _router;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт конвейер обработки обновлений.
|
||||||
|
/// </summary>
|
||||||
|
public Pipeline(IEnumerable<IUpdateMiddleware> middlewares, IRouter router)
|
||||||
|
{
|
||||||
|
_middlewares = middlewares.ToList();
|
||||||
|
_router = router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запускает выполнение конвейера для заданного контекста.
|
||||||
|
/// </summary>
|
||||||
|
public Task ExecuteAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var index = 0;
|
||||||
|
Task Next() => (index < _middlewares.Count)
|
||||||
|
? _middlewares[index++].InvokeAsync(ctx, Next, ct)
|
||||||
|
: _router.RouteAsync(ctx, ct);
|
||||||
|
|
||||||
|
return Next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
BotPages.Core/Pipeline/Router.cs
Normal file
45
BotPages.Core/Pipeline/Router.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Простой роутер: команды верхнего уровня и делегирование текущей странице.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Router : IRouter
|
||||||
|
{
|
||||||
|
private readonly IPageRegistry _pages;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт роутер страниц.
|
||||||
|
/// </summary>
|
||||||
|
public Router(IPageRegistry pages) => _pages = pages;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет текущую страницу и вызывает её обработчик.
|
||||||
|
/// </summary>
|
||||||
|
public async Task RouteAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var text = ctx.Text ?? string.Empty;
|
||||||
|
|
||||||
|
if (text.StartsWith("/start", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
await ctx.Nav.ReplaceAsync(_pages.DefaultPage.Id, null, ctx, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var current = (await ctx.Nav.CurrentAsync(ctx, ct))?.PageId;
|
||||||
|
if (current is not null)
|
||||||
|
{
|
||||||
|
var pr = await _pages.Get(current).HandleAsync(ctx, ct);
|
||||||
|
await ctx.Nav.ApplyResultAsync(ctx, pr, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ctx.Nav.ReplaceAsync(_pages.DefaultPage.Id, null, ctx, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Вынести в "дефолтный /start page"
|
||||||
|
await ctx.Client.SendTextAsync(ctx.Chat.Id, "Не понимаю. Нажмите /start.", null, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
BotPages.Core/Pipeline/ThrottleMiddleware.cs
Normal file
25
BotPages.Core/Pipeline/ThrottleMiddleware.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware троттлинга для ограничений нагрузки.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ThrottleMiddleware : IUpdateMiddleware
|
||||||
|
{
|
||||||
|
private readonly TimeSpan _delay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт middleware троттлинга.
|
||||||
|
/// </summary>
|
||||||
|
public ThrottleMiddleware(TimeSpan delay) => _delay = delay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавляет искусственную задержку перед продолжением обработки.
|
||||||
|
/// </summary>
|
||||||
|
public async Task InvokeAsync(UpdateContext ctx, Func<Task> next, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await Task.Delay(_delay, ct);
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
BotPages.Core/Transport/DefaultFileService.cs
Normal file
43
BotPages.Core/Transport/DefaultFileService.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Транспорт-независимая реализация отправки пачек через клиент.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DefaultFileService : IFileService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Заглушка загрузки файла (реализуется в адаптере транспорта).
|
||||||
|
/// </summary>
|
||||||
|
public Task<FileDescriptor> DownloadAsync(string fileId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
throw new System.NotImplementedException("DownloadAsync должен быть реализован конкретным транспортным адаптером.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Загружает несколько файлов по идентификаторам.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<FileDescriptor>> DownloadManyAsync(IEnumerable<string> fileIds, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var res = new List<FileDescriptor>();
|
||||||
|
foreach (var id in fileIds)
|
||||||
|
res.Add(await DownloadAsync(id, ct));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет один файл через клиент.
|
||||||
|
/// </summary>
|
||||||
|
public Task SendAsync(IChatClient client, long chatId, FileDescriptor file, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return client.SendFilesAsync(chatId, new[] { file }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет несколько файлов через клиент.
|
||||||
|
/// </summary>
|
||||||
|
public Task SendManyAsync(IChatClient client, long chatId, IEnumerable<FileDescriptor> files, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return client.SendFilesAsync(chatId, files, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
BotPages.Core/Transport/IChatClient.cs
Normal file
27
BotPages.Core/Transport/IChatClient.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Универсальный клиент для отправки сообщений и файлов в чат.
|
||||||
|
/// Адаптеры (Telegram, MAX и др.) реализуют этот интерфейс.
|
||||||
|
/// </summary>
|
||||||
|
public interface IChatClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет текстовое сообщение.
|
||||||
|
/// Может сопровождаться клавиатурой (inline или reply).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chatId">Идентификатор чата.</param>
|
||||||
|
/// <param name="message">Сообщение.</param>
|
||||||
|
/// <param name="actions">Кнопки для отображения (опционально).</param>
|
||||||
|
/// <param name="ct">Токен отмены.</param>
|
||||||
|
Task SendTextAsync(long chatId, PageMessage message, IEnumerable<PageAction>? actions, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет файлы в чат.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chatId">Идентификатор чата.</param>
|
||||||
|
/// <param name="files">Файлы для отправки.</param>
|
||||||
|
/// <param name="ct">Токен отмены.</param>
|
||||||
|
Task SendFilesAsync(long chatId, IEnumerable<FileDescriptor> files, CancellationToken ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
BotPages.Core/Transport/IFileService.cs
Normal file
28
BotPages.Core/Transport/IFileService.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace BotPages.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Сервис работы с файлами: загрузка и отправка пакетами.
|
||||||
|
/// </summary>
|
||||||
|
public interface IFileService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Загружает файл по идентификатору транспорта.
|
||||||
|
/// </summary>
|
||||||
|
Task<FileDescriptor> DownloadAsync(string fileId, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Загружает несколько файлов по их идентификаторам.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<FileDescriptor>> DownloadManyAsync(IEnumerable<string> fileIds, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет один файл в чат.
|
||||||
|
/// </summary>
|
||||||
|
Task SendAsync(IChatClient client, long chatId, FileDescriptor file, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет несколько файлов в чат.
|
||||||
|
/// </summary>
|
||||||
|
Task SendManyAsync(IChatClient client, long chatId, IEnumerable<FileDescriptor> files, CancellationToken ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
BotPages.Telegram/BotPages.Telegram.csproj
Normal file
17
BotPages.Telegram/BotPages.Telegram.csproj
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Telegram.Bot" Version="22.7.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\BotPages.Core\BotPages.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
89
BotPages.Telegram/TelegramClientAdapter.cs
Normal file
89
BotPages.Telegram/TelegramClientAdapter.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using BotPages.Core;
|
||||||
|
using Telegram.Bot;
|
||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegram.Bot.Types.ReplyMarkups;
|
||||||
|
using static System.Net.Mime.MediaTypeNames;
|
||||||
|
|
||||||
|
namespace BotPages.Telegram
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Адаптер клиента для Telegram Bot API.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TelegramClientAdapter : IChatClient
|
||||||
|
{
|
||||||
|
private readonly ITelegramBotClient _bot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт адаптер на основе ITelegramBotClient.
|
||||||
|
/// </summary>
|
||||||
|
public TelegramClientAdapter(ITelegramBotClient bot) => _bot = bot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет текстовое сообщение с опциональной клавиатурой.
|
||||||
|
/// </summary>
|
||||||
|
public Task SendTextAsync(long chatId, PageMessage message, IEnumerable<PageAction>? actions, CancellationToken ct)
|
||||||
|
{
|
||||||
|
ReplyMarkup? replyMarkup = null;
|
||||||
|
|
||||||
|
if (actions is { })
|
||||||
|
{
|
||||||
|
var inlineGroups = actions
|
||||||
|
.Where(a => a.Placement == ActionPlacement.Inline)
|
||||||
|
.GroupBy(a => a.Row)
|
||||||
|
.OrderBy(g => g.Key)
|
||||||
|
.Select(g => g.Select(a => InlineKeyboardButton.WithCallbackData(a.Label, a.Value)).ToArray())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var replyGroups = actions
|
||||||
|
.Where(a => a.Placement == ActionPlacement.Reply)
|
||||||
|
.GroupBy(a => a.Row)
|
||||||
|
.OrderBy(g => g.Key)
|
||||||
|
.Select(g => g.Select(a => new KeyboardButton(a.Label)).ToArray())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (inlineGroups.Any())
|
||||||
|
replyMarkup = new InlineKeyboardMarkup(inlineGroups);
|
||||||
|
else if (replyGroups.Any())
|
||||||
|
replyMarkup = new ReplyKeyboardMarkup(replyGroups) { ResizeKeyboard = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
var parseMode = message.Format switch
|
||||||
|
{
|
||||||
|
MessageFormat.Markdown => ParseMode.MarkdownV2,
|
||||||
|
MessageFormat.Html => ParseMode.Html,
|
||||||
|
_ => ParseMode.None,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return _bot.SendMessage(new ChatId(chatId),
|
||||||
|
message.Text,
|
||||||
|
parseMode: parseMode,
|
||||||
|
replyMarkup: replyMarkup,
|
||||||
|
disableNotification: message.IsSilent,
|
||||||
|
cancellationToken: ct
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет файлы как документы (по одному или пачкой).
|
||||||
|
/// </summary>
|
||||||
|
public async Task SendFilesAsync(long chatId, IEnumerable<FileDescriptor> files, CancellationToken ct)
|
||||||
|
{
|
||||||
|
foreach (var f in files)
|
||||||
|
{
|
||||||
|
if (f.Content is not null)
|
||||||
|
{
|
||||||
|
var input = new InputFileStream(f.Content, f.Name);
|
||||||
|
await _bot.SendDocument(chatId, input, cancellationToken: ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Если контент не загружен, и есть FileId — отправляем по Id
|
||||||
|
await _bot.SendDocument(chatId, new InputFileId(f.Id), cancellationToken: ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
BotPages.Telegram/TelegramFileService.cs
Normal file
59
BotPages.Telegram/TelegramFileService.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using BotPages.Core;
|
||||||
|
using Telegram.Bot;
|
||||||
|
|
||||||
|
namespace BotPages.Telegram
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FileService для Telegram: загрузка по FileId.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TelegramFileService : IFileService
|
||||||
|
{
|
||||||
|
private readonly ITelegramBotClient _bot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт файловый сервис для Telegram.
|
||||||
|
/// </summary>
|
||||||
|
public TelegramFileService(ITelegramBotClient bot) => _bot = bot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Загружает файл по идентификатору Telegram FileId.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<FileDescriptor> DownloadAsync(string fileId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var file = await _bot.GetFile(fileId, ct);
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
await _bot.DownloadFile(file.FilePath!, ms, ct);
|
||||||
|
ms.Position = 0;
|
||||||
|
var name = System.IO.Path.GetFileName(file.FilePath!);
|
||||||
|
return new FileDescriptor(file.FileId, name, "application/octet-stream", ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Загружает несколько файлов по их идентификаторам.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<FileDescriptor>> DownloadManyAsync(IEnumerable<string> fileIds, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var res = new List<FileDescriptor>();
|
||||||
|
foreach (var id in fileIds)
|
||||||
|
res.Add(await DownloadAsync(id, ct));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет один файл в чат.
|
||||||
|
/// </summary>
|
||||||
|
public async Task SendAsync(IChatClient client, long chatId, FileDescriptor file, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await client.SendFilesAsync(chatId, new[] { file }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отправляет несколько файлов в чат.
|
||||||
|
/// </summary>
|
||||||
|
public Task SendManyAsync(IChatClient client, long chatId, IEnumerable<FileDescriptor> files, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return client.SendFilesAsync(chatId, files, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
BotPages.Telegram/TelegramUpdateMapper.cs
Normal file
46
BotPages.Telegram/TelegramUpdateMapper.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using BotPages.Core;
|
||||||
|
using Telegram.Bot;
|
||||||
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
|
namespace BotPages.Telegram
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Утилиты для извлечения контекста из Telegram Update.
|
||||||
|
/// </summary>
|
||||||
|
public static class TelegramUpdateMapper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Преобразует Telegram Update в универсальный UpdateContext.
|
||||||
|
/// </summary>
|
||||||
|
public static UpdateContext Map(ITelegramBotClient bot, INavigationService nav, IStateStore store, Update update)
|
||||||
|
{
|
||||||
|
var chat = update.Message?.Chat ?? update.CallbackQuery?.Message?.Chat;
|
||||||
|
var user = update.Message?.From ?? update.CallbackQuery?.From;
|
||||||
|
|
||||||
|
var text = update.Message?.Text ?? update.CallbackQuery?.Data;
|
||||||
|
|
||||||
|
var files = new List<FileDescriptor>();
|
||||||
|
if (update.Message?.Document is { } doc)
|
||||||
|
{
|
||||||
|
files.Add(new FileDescriptor(doc.FileId, doc.FileName ?? "file", doc.MimeType ?? "application/octet-stream"));
|
||||||
|
}
|
||||||
|
if (update.Message?.Photo is { } photos && photos.Count() > 0)
|
||||||
|
{
|
||||||
|
var largest = photos.OrderBy(p => p.FileSize).Last();
|
||||||
|
files.Add(new FileDescriptor(largest.FileId, "photo.jpg", "image/jpeg"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UpdateContext
|
||||||
|
{
|
||||||
|
Client = new TelegramClientAdapter(bot),
|
||||||
|
Chat = new ChatContext { Id = chat!.Id, Title = chat.Title },
|
||||||
|
User = new UserContext { Id = user!.Id, DisplayName = $"{user.FirstName} {user.LastName}" },
|
||||||
|
Text = text,
|
||||||
|
IncomingFiles = files,
|
||||||
|
RawUpdate = update,
|
||||||
|
Nav = nav,
|
||||||
|
State = store
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
BotPages.slnx
Normal file
6
BotPages.slnx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="BotPages.Core/BotPages.Core.csproj" />
|
||||||
|
<Project Path="BotPages.Telegram/BotPages.Telegram.csproj" />
|
||||||
|
<Project Path="BotPages/BotPages.csproj" />
|
||||||
|
<Project Path="Demo/Demo.csproj" />
|
||||||
|
</Solution>
|
||||||
14
BotPages/BotPages.csproj
Normal file
14
BotPages/BotPages.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\BotPages.Core\BotPages.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\BotPages.Telegram\BotPages.Telegram.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
14
Demo/Demo.csproj
Normal file
14
Demo/Demo.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\BotPages\BotPages.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
39
Demo/Pages/FilesPage.cs
Normal file
39
Demo/Pages/FilesPage.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using BotPages.Core;
|
||||||
|
|
||||||
|
namespace Demo.Pages
|
||||||
|
{
|
||||||
|
public sealed class FilesPage : Page
|
||||||
|
{
|
||||||
|
public static string Id => nameof(FilesPage);
|
||||||
|
|
||||||
|
public override Task<PageResult> EnterAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var actions = new[]
|
||||||
|
{
|
||||||
|
new PageAction { Label = "⬅️ Назад", Value = "back", Placement = ActionPlacement.Reply, Row = 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.FromResult(
|
||||||
|
PageResultBuilder.Empty()
|
||||||
|
.WithText("📂 Здесь можно загрузить или отправить файл.")
|
||||||
|
.WithKeyboard(actions)
|
||||||
|
.Build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<PageResult> HandleAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (ctx.Text == "⬅️ Назад")
|
||||||
|
return PageResultBuilder.Empty().WithNavigate(nameof(MainPage)).Build();
|
||||||
|
|
||||||
|
if (ctx.IncomingFiles?.Count > 0)
|
||||||
|
{
|
||||||
|
await ctx.Client.SendFilesAsync(ctx.Chat.Id, ctx.IncomingFiles, ct);
|
||||||
|
return PageResultBuilder.Empty().WithText("Файл получен и отправлен обратно.").Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return PageResultBuilder.Empty().WithText("Пришлите файл или нажмите 'Назад'.").Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
Demo/Pages/InlinePage.cs
Normal file
35
Demo/Pages/InlinePage.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using BotPages.Core;
|
||||||
|
|
||||||
|
namespace Demo.Pages
|
||||||
|
{
|
||||||
|
public sealed class InlinePage : Page
|
||||||
|
{
|
||||||
|
public override string Id => nameof(InlinePage);
|
||||||
|
|
||||||
|
public override Task<PageResult> EnterAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var actions = new[]
|
||||||
|
{
|
||||||
|
new PageAction { Label = "⬅️ Назад", Value = "back", Placement = ActionPlacement.Inline, Row = 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.FromResult(
|
||||||
|
PageResultBuilder.Empty()
|
||||||
|
.WithText("Это страница с Inline‑кнопками.")
|
||||||
|
.WithKeyboard(actions)
|
||||||
|
.Build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<PageResult> HandleAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (ctx.Text == "back")
|
||||||
|
return Task.FromResult(PageResultBuilder.Empty().WithNavigate(nameof(MainPage)).Build());
|
||||||
|
|
||||||
|
return Task.FromResult(PageResultBuilder.Empty().WithText("Нажмите кнопку 'Назад'.").Build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
38
Demo/Pages/MainPage.cs
Normal file
38
Demo/Pages/MainPage.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using BotPages.Core;
|
||||||
|
|
||||||
|
namespace Demo.Pages
|
||||||
|
{
|
||||||
|
public sealed class MainPage : Page
|
||||||
|
{
|
||||||
|
public override string Id => nameof(MainPage);
|
||||||
|
|
||||||
|
public override Task<PageResult> EnterAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var actions = new[]
|
||||||
|
{
|
||||||
|
new PageAction { Label = "📌 Inline", Value = "inline", Placement = ActionPlacement.Reply, Row = 0 },
|
||||||
|
new PageAction { Label = "⌨️ Reply", Value = "reply", Placement = ActionPlacement.Reply, Row = 1 },
|
||||||
|
new PageAction { Label = "📂 Файлы", Value = "files", Placement = ActionPlacement.Reply, Row = 2 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.FromResult(
|
||||||
|
PageResultBuilder.Empty()
|
||||||
|
.WithText("🏠 Главная страница.\nВыберите куда перейти:")
|
||||||
|
.WithKeyboard(actions)
|
||||||
|
.Build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<PageResult> HandleAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return ctx.Text switch
|
||||||
|
{
|
||||||
|
"📌 Inline" => Task.FromResult(PageResultBuilder.Empty().WithNavigate(nameof(InlinePage)).Build()),
|
||||||
|
"⌨️ Reply" => Task.FromResult(PageResultBuilder.Empty().WithNavigate(nameof(ReplyPage)).Build()),
|
||||||
|
"📂 Файлы" => Task.FromResult(PageResultBuilder.Empty().WithNavigate(nameof(FilesPage)).Build()),
|
||||||
|
_ => Task.FromResult(PageResultBuilder.Empty().WithText("Выберите действие с кнопок.").Build())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
33
Demo/Pages/ReplyPage.cs
Normal file
33
Demo/Pages/ReplyPage.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using BotPages.Core;
|
||||||
|
|
||||||
|
namespace Demo.Pages
|
||||||
|
{
|
||||||
|
public sealed class ReplyPage : Page
|
||||||
|
{
|
||||||
|
public override string Id => nameof(ReplyPage);
|
||||||
|
|
||||||
|
public override Task<PageResult> EnterAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var actions = new[]
|
||||||
|
{
|
||||||
|
new PageAction { Label = "⬅️ Назад", Value = "back", Placement = ActionPlacement.Reply, Row = 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.FromResult(
|
||||||
|
PageResultBuilder.Empty()
|
||||||
|
.WithText("Это страница с Reply‑клавиатурой.")
|
||||||
|
.WithKeyboard(actions)
|
||||||
|
.Build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<PageResult> HandleAsync(UpdateContext ctx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (ctx.Text == "⬅️ Назад")
|
||||||
|
return Task.FromResult(PageResultBuilder.Empty().WithNavigate(nameof(MainPage)).Build());
|
||||||
|
|
||||||
|
return Task.FromResult(PageResultBuilder.Empty().WithText("Нажмите кнопку 'Назад'.").Build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
65
Demo/Program.cs
Normal file
65
Demo/Program.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using BotPages.Core;
|
||||||
|
using BotPages.Telegram;
|
||||||
|
using Demo.Pages;
|
||||||
|
using Telegram.Bot;
|
||||||
|
|
||||||
|
namespace Demo
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
public static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
// Токен Telegram бота
|
||||||
|
var token = Environment.GetEnvironmentVariable("TELEGRAM_TOKEN") ?? throw new InvalidOperationException("TELEGRAM_TOKEN not set");
|
||||||
|
|
||||||
|
// Инициализация Telegram клиента
|
||||||
|
var botClient = new TelegramBotClient(token);
|
||||||
|
var chatClient = new TelegramClientAdapter(botClient);
|
||||||
|
|
||||||
|
|
||||||
|
// Регистрируем страницы
|
||||||
|
var pages = new IPage[]
|
||||||
|
{
|
||||||
|
new MainPage(),
|
||||||
|
new InlinePage(),
|
||||||
|
new ReplyPage(),
|
||||||
|
new FilesPage()
|
||||||
|
};
|
||||||
|
var registry = new PageRegistry(pages, pages[0]);
|
||||||
|
|
||||||
|
// Навигация и состояние
|
||||||
|
IStateStore store = new InMemoryStateStore();
|
||||||
|
INavigationService nav = new NavigationService(registry, store);
|
||||||
|
var router = new Router(registry);
|
||||||
|
|
||||||
|
var middleware = new IUpdateMiddleware[]
|
||||||
|
{
|
||||||
|
new LoggingMiddleware(), //логирование вызова в консоль
|
||||||
|
new ErrorHandlingMiddleware(), //обработчик ошибок
|
||||||
|
//new ThrottleMiddleware(TimeSpan.FromMilliseconds(150)), //задержка в 150мс перед ответом
|
||||||
|
};
|
||||||
|
|
||||||
|
var pipeline = new Pipeline(middleware, router);
|
||||||
|
|
||||||
|
botClient.StartReceiving(
|
||||||
|
async (bot, update, ct) =>
|
||||||
|
{
|
||||||
|
var ctx = TelegramUpdateMapper.Map(bot, nav, store, update);
|
||||||
|
|
||||||
|
await pipeline.ExecuteAsync(ctx, ct);
|
||||||
|
},
|
||||||
|
(bot, error, ct) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"⚠️ Ошибка Telegram: {error}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var me = await botClient.GetMe();
|
||||||
|
|
||||||
|
Console.WriteLine($"BotPages Demo (@{me.Username}) запущен. Нажмите Enter для выхода.");
|
||||||
|
Console.ReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Demo/Properties/launchSettings.json
Normal file
10
Demo/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"Demo": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"environmentVariables": {
|
||||||
|
"TELEGRAM_TOKEN": "7992309062:AAHkb4wnFi8w7H4V0zvUW_LJg55jtzuhahU"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user