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, false, null); return this; } /// /// Зарегистрировать команду, ведущую на страницу. /// public BotPagesApp MapCommand(string commandTemplate, bool publish, string description) where TPage : Page { _commands.Map(commandTemplate, publish, description); return this; } /// /// Зарегистрировать команду с кастомным обработчиком. /// public BotPagesApp MapCommand(string template, CommandHandler handler) { _commands.Map(template, handler, false, null); return this; } /// /// Зарегистрировать команду с кастомным обработчиком. /// public BotPagesApp MapCommand(string template, CommandHandler handler, bool publish, string description) { _commands.Map(template, handler, publish, description); 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) { 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.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); } 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), _commands.Commands, cancellationToken); } } }