namespace BotPages.Core;
using BotPages.Core.Abstractions;
using BotPages.Core.Context;
using BotPages.Core.Logging;
using BotPages.Core.Routing;
using System.Reflection;
///
/// Основное приложение BotPages.
/// Управляет маршрутизацией, командами, middleware и страницами.
///
public sealed class BotPagesApp
{
private readonly IMessengerAdapterFactory _adapterFactory;
private readonly List _middlewares = new();
private readonly RoutesRegistry _routes = new();
private readonly CommandsRegistry _commands = new();
private readonly IStateStorage _state;
private readonly ILogger _logger;
private readonly NavigationService _navigation;
///
/// Серсвис логирования.
///
public ILogger Logger => _logger;
///
/// Создать приложение BotPages.
///
public BotPagesApp(IStateStorage state, ILogger logger)
{
_state = state;
_logger = logger;
_navigation = new NavigationService(_routes);
_adapterFactory = new MultiAdapterFactory();
}
///
/// Добавить адаптер.
///
public BotPagesApp AddAdapter(string messengerType, IMessangerAdapterSetup adapter)
{
_adapterFactory.Register(messengerType, adapter);
return this;
}
///
/// Установить страницу по умолчанию.
///
public BotPagesApp AddDefaultPage() where TPage : SingletonPage
{
_navigation.AddDefaultPage();
return this;
}
///
/// Добавить middleware.
///
public BotPagesApp AddMiddleware(TMiddleware instance) where TMiddleware : IPageMiddleware
{
_middlewares.Add(instance);
return this;
}
///
/// Зарегистрировать команду, ведущую на страницу.
///
public BotPagesApp MapCommand(string commandTemplate) where TPage : Page
{
_commands.Map(commandTemplate);
return this;
}
///
/// Зарегистрировать команду с кастомным обработчиком.
///
public BotPagesApp MapCommand(string template, Func handler)
{
_commands.Map(template, handler);
return this;
}
///
/// Зарегистрировать маршрут для страницы.
///
public BotPagesApp MapRoute(string template) where TPage : Page
{
_routes.Map(template);
return this;
}
///
/// Зарегистрировать все маршруты для страницы.
/// Маршрутом является .
/// Так же берется полное название класса.
///
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;
}
///
/// Обработать входящее обновление.
///
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 BuildPipeline(PageContext ctx, Func terminal)
{
Func next = terminal;
foreach (var mw in _middlewares.AsEnumerable().Reverse())
{
var prev = next;
next = () => mw.InvokeAsync(ctx, prev, _currentCt);
}
return next;
}
// Технические поля для конвейера
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);
}
///
/// Отправить обновление на текущую страницу.
///
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);
}
}
///
/// Определить текущую страницу.
///
private Page? ResolveCurrentPage(PageContext ctx)
=> _navigation.ResolveCurrentPage(ctx);
///
/// Сборка и запуск приложения.
///
///
///
public async Task Build(CancellationToken cancellationToken)
{
foreach (var adapter in _adapterFactory.Adapters)
{
await adapter.Value.StartAdapterAsync(update => HandleUpdateAsync(update, cancellationToken), cancellationToken);
}
}
}