Files
BotPages/BotPages.Core/BotPagesApp.cs

289 lines
9.7 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;
using System.Reflection;
/// <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>
/// Серсвис логирования.
/// </summary>
public ILogger Logger => _logger;
/// <summary>
/// Создать приложение BotPages.
/// </summary>
public BotPagesApp(IStateStorage state, ILogger logger)
{
_state = state;
_logger = logger;
_navigation = new NavigationService(_routes);
_adapterFactory = new MultiAdapterFactory();
}
/// <summary>
/// Добавить адаптер с указанием ID.
/// </summary>
/// <exception cref="ArgumentException">Если адаптер с таким ID уже существует.</exception>
public BotPagesApp AddAdapter(string adapterId, IMessengerAdapterSetup adapter)
{
_adapterFactory.Register(adapterId, adapter);
return this;
}
/// <summary>
/// Добавить адаптер с автоматическим ID.
/// </summary>
public BotPagesApp AddAdapter(IMessengerAdapterSetup adapter)
{
_adapterFactory.Register(adapter);
return this;
}
/// <summary>
/// Проверить, существует ли адаптер с указанным ID.
/// </summary>
public bool HasAdapter(string adapterId) => _adapterFactory.Contains(adapterId);
/// <summary>
/// Получить адаптер по ID.
/// </summary>
public IMessengerAdapter GetAdapter(string adapterId) => _adapterFactory.Resolve(adapterId);
/// <summary>
/// Получить все адаптеры определенного типа.
/// </summary>
public IReadOnlyList<IMessengerAdapter> GetAdaptersByType(string adapterType)
=> _adapterFactory.GetAdaptersByType(adapterType);
/// <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, false, null);
return this;
}
/// <summary>
/// Зарегистрировать команду, ведущую на страницу.
/// </summary>
public BotPagesApp MapCommand<TPage>(string commandTemplate, bool publish, string description) where TPage : Page
{
_commands.Map<TPage>(commandTemplate, publish, description);
return this;
}
/// <summary>
/// Зарегистрировать команду с кастомным обработчиком.
/// </summary>
public BotPagesApp MapCommand(string template, CommandHandler handler)
{
_commands.Map(template, handler, false, null);
return this;
}
/// <summary>
/// Зарегистрировать команду с кастомным обработчиком.
/// </summary>
public BotPagesApp MapCommand(string template, CommandHandler handler, bool publish, string description)
{
_commands.Map(template, handler, publish, description);
return this;
}
/// <summary>
/// Зарегистрировать маршрут для страницы.
/// </summary>
public BotPagesApp MapRoute<TPage>(string template) where TPage : Page
{
_routes.Map<TPage>(template);
return this;
}
/// <summary>
/// Зарегистрировать все маршруты для страницы.
/// Маршрутом является <see cref="RouteAttribute"/>.
/// Так же берется полное название класса.
/// </summary>
public BotPagesApp AutoMapRoute()
{
// Берём все загруженные сборки в текущем AppDomain
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
// Находим все типы, которые наследуются от Page
var pageTypes = assemblies
.SelectMany(a =>
{
try
{
return a.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
// Если часть типов не загрузилась — берём только успешные
return ex.Types.Where(t => t != null)!;
}
})
.Where(t => t != null
&& t.IsClass
&& !t.IsAbstract
&& t.IsSubclassOf(typeof(Page)))
.ToList();
// Выводим полные имена
foreach (var type in pageTypes)
{
_routes.Map(type);
}
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)
{
await dispatched;
return;
}
}
// Конвейер middleware
var pipeline = BuildPipeline(ctx, async () =>
{
await DispatchToPageAsync(ctx, update, ct);
});
await pipeline();
}
/// <summary>
/// Создать контекст страницы для текущего обновления.
/// </summary>
private async Task<PageContext> 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.Adapter.Id),
AdapterFactory = _adapterFactory,
};
return await Task.FromResult(ctx);
}
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 DispatchToPageAsync(PageContext ctx, UpdateContext update, CancellationToken ct)
{
var page = ResolveCurrentPage(ctx);
if (page is null)
{
await ctx.Navigation.GoToHomeAsync(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);
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)
{
_logger.Log(LogLevel.Critical, "Unhandled page error.", ex);
await page.OnError(ctx, ex, ct);
}
}
/// <summary>
/// Определить текущую страницу.
/// </summary>
private Page? ResolveCurrentPage(PageContext ctx)
=> _navigation.ResolveCurrentPage(ctx);
/// <summary>
/// Сборка и запуск приложения.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task Build(CancellationToken cancellationToken)
{
foreach (var adapter in _adapterFactory.AllAdapters)
{
await adapter.StartAdapterAsync(update => HandleUpdateAsync(update, cancellationToken), _commands.Commands, cancellationToken);
}
}
}