Files
BotPages/BotPages.Core/BotPagesApp.cs
FrigaT e6e5459280
All checks were successful
CI / build-test (push) Successful in 30s
Доработано Demo
2025-12-05 13:33:25 +03:00

176 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace BotPages.Core;
using BotPages.Core.Abstractions;
using BotPages.Core.Context;
using BotPages.Core.Logging;
using BotPages.Core.Routing;
/// <summary>
/// Основное приложение BotPages.
/// Управляет маршрутизацией, командами, middleware и страницами.
/// </summary>
public sealed class BotPagesApp
{
private readonly IMessengerAdapterFactory _adapterFactory;
private readonly List<IPageMiddleware> _middlewares = new();
private readonly RoutesRegistry _routes = new();
private readonly CommandsRegistry _commands = new();
private readonly IStateStorage _state;
private readonly ILogger _logger;
private readonly NavigationService _navigation;
/// <summary>
/// Создать приложение BotPages.
/// </summary>
public BotPagesApp(IMessengerAdapterFactory adapterFactory, IStateStorage state, ILogger logger)
{
_adapterFactory = adapterFactory;
_state = state;
_logger = logger;
_navigation = new NavigationService(_routes);
}
/// <summary>
/// Установить страницу по умолчанию.
/// </summary>
public BotPagesApp AddDefaultPage<TPage>() where TPage : SingletonPage
{
_navigation.AddDefaultPage<TPage>();
return this;
}
/// <summary>
/// Добавить middleware.
/// </summary>
public BotPagesApp AddMiddleware<TMiddleware>(TMiddleware instance) where TMiddleware : IPageMiddleware
{
_middlewares.Add(instance);
return this;
}
/// <summary>
/// Зарегистрировать команду, ведущую на страницу.
/// </summary>
public BotPagesApp MapCommand<TPage>(string commandTemplate) where TPage : Page
{
_commands.Map<TPage>(commandTemplate);
return this;
}
/// <summary>
/// Зарегистрировать команду с кастомным обработчиком.
/// </summary>
public BotPagesApp MapCommand(string template, Func<PageContext, CancellationToken, Task> handler)
{
_commands.Map(template, handler);
return this;
}
/// <summary>
/// Зарегистрировать маршрут для страницы.
/// </summary>
public BotPagesApp MapRoute<TPage>(string template) where TPage : Page
{
_routes.Map<TPage>(template);
return this;
}
/// <summary>
/// Обработать входящее обновление.
/// </summary>
public async Task HandleUpdateAsync(UpdateContext update, CancellationToken ct)
{
var ctx = await CreatePageContextAsync(update, ct);
// Команды выше событий страниц
if (update.Kind == UpdateKind.Text && update.Text is not null && update.Text.StartsWith("/"))
{
if (_commands.TryDispatch(ctx, update.Text, ct, out var dispatched) && dispatched is not null)
{
_logger.Log(LogLevel.Info, $"Command '{update.Text}' dispatched.");
await dispatched;
return;
}
}
// Конвейер middleware
var pipeline = BuildPipeline(ctx, async () =>
{
await DispatchToPageAsync(ctx, update, ct);
});
await pipeline();
}
private Func<Task> BuildPipeline(PageContext ctx, Func<Task> terminal)
{
Func<Task> next = terminal;
foreach (var mw in _middlewares.AsEnumerable().Reverse())
{
var prev = next;
next = () => mw.InvokeAsync(ctx, prev, _currentCt);
}
return next;
}
// Технические поля для конвейера
private CancellationToken _currentCt;
/// <summary>
/// Создать контекст страницы для текущего обновления.
/// </summary>
private async Task<PageContext> 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);
}
/// <summary>
/// Отправить обновление на текущую страницу.
/// </summary>
private async Task DispatchToPageAsync(PageContext ctx, UpdateContext update, CancellationToken ct)
{
var page = ResolveCurrentPage(ctx);
if (page is null)
{
await ctx.Navigation.GoToHome(ctx, ct);
return;
}
try
{
await page.OnUpdate(ctx, update, ct);
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);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Critical, "Unhandled page error.", ex);
await page.OnError(ctx, ex, ct);
}
}
/// <summary>
/// Определить текущую страницу.
/// </summary>
private Page? ResolveCurrentPage(PageContext ctx)
=> _navigation.ResolveCurrentPage(ctx);
}