namespace BotPages.Core; using BotPages.Core.Abstractions; using BotPages.Core.Context; using BotPages.Core.Logging; using BotPages.Core.Routing; using System.Reflection; using System.Threading; /// /// Основное приложение BotPages. /// Управляет маршрутизацией, командами, middleware и страницами. /// public sealed class BotPagesApp : IDisposable, IAsyncDisposable { 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; private CancellationTokenSource? _internalCts; private bool _isRunning; private bool _disposed; /// /// Серсвис логирования. /// public ILogger Logger => _logger; /// /// ТОкен отмены приложения. Отменяется при вызове Stop() или при отмене внешнего токена, переданного в RunAsync. /// public CancellationToken CancellationToken => _internalCts?.Token ?? CancellationToken.None; /// /// Создать приложение BotPages. /// public BotPagesApp(IStateStorage state, ILogger logger) { _state = state; _logger = logger; _navigation = new NavigationService(_routes); _adapterFactory = new MultiAdapterFactory(); } /// /// Добавить адаптер с указанием ID. /// /// Если адаптер с таким ID уже существует. public BotPagesApp AddAdapter(string adapterId, IMessengerAdapterSetup adapter) { _adapterFactory.Register(adapterId, adapter); return this; } /// /// Добавить адаптер с автоматическим ID. /// public BotPagesApp AddAdapter(IMessengerAdapterSetup adapter) { _adapterFactory.Register(adapter); return this; } /// /// Проверить, существует ли адаптер с указанным ID. /// public bool HasAdapter(string adapterId) => _adapterFactory.Contains(adapterId); /// /// Получить адаптер по ID. /// public IMessengerAdapter GetAdapter(string adapterId) => _adapterFactory.Resolve(adapterId); /// /// Получить все адаптеры определенного типа. /// public IReadOnlyList GetAdaptersByType(string adapterType) => _adapterFactory.GetAdaptersByType(adapterType); /// /// Установить страницу по умолчанию. /// 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) { var ctx = await CreatePageContextAsync(update); // Команды выше событий страниц if (update.Kind == UpdateKind.Text && update.Text is not null && update.Text.StartsWith("/")) { if (_commands.TryDispatch(ctx, update.Text, this.CancellationToken, out var dispatched) && dispatched is not null) { await dispatched; return; } } // Конвейер middleware var pipeline = BuildPipeline(ctx, async () => { await DispatchToPageAsync(ctx, update); }); await pipeline(); } /// /// Создать контекст страницы для текущего обновления. /// private async Task CreatePageContextAsync(UpdateContext update) { 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 BuildPipeline(PageContext ctx, Func terminal) { Func next = terminal; foreach (var mw in _middlewares.AsEnumerable().Reverse()) { var prev = next; next = () => mw.InvokeAsync(ctx, prev, this.CancellationToken); } return next; } /// /// Отправить обновление на текущую страницу. /// private async Task DispatchToPageAsync(PageContext ctx, UpdateContext update) { var page = ResolveCurrentPage(ctx); if (page is null) { await ctx.Navigation.GoToHomeAsync(ctx, this.CancellationToken); return; } try { await page.OnUpdate(ctx, update, this.CancellationToken); if (update.Kind.HasFlag(UpdateKind.Text) && update.Text is not null) await page.OnText(ctx, update.Text, this.CancellationToken); if (update.Kind.HasFlag(UpdateKind.Button) && update.Text is not null) await page.OnButton(ctx, update.Text, this.CancellationToken); if (update.Kind.HasFlag(UpdateKind.File) && update.Files.Count > 0) await page.OnFile(ctx, update.Files, this.CancellationToken); if (update.Kind.HasFlag(UpdateKind.Pin) && update.PinInfo is not null) await page.OnPin(ctx, update.PinInfo, this.CancellationToken); if (update.Kind.HasFlag(UpdateKind.Delete) && update.DeleteInfo is not null) await page.OnDelete(ctx, update.DeleteInfo, this.CancellationToken); } catch (Exception ex) { _logger.Log(LogLevel.Critical, "Unhandled page error.", ex); await page.OnError(ctx, ex, this.CancellationToken); } } /// /// Определить текущую страницу. /// private Page? ResolveCurrentPage(PageContext ctx) => _navigation.ResolveCurrentPage(ctx); /// /// Запустить приложение и синхронно ожидать его завершения. /// /// Токен отмены для остановки приложения извне public void Run(CancellationToken ct = default) { RunAsync(ct).Wait(); } /// /// Сборка и запуск приложения. /// /// public async Task RunAsync() => await RunAsync(new CancellationTokenSource().Token); /// /// Сборка и запуск приложения. /// /// public async Task RunAsync(CancellationToken ct = default) { if (_isRunning) throw new InvalidOperationException("Application is already running."); if (_disposed) throw new ObjectDisposedException(nameof(BotPagesApp)); _internalCts = CancellationTokenSource.CreateLinkedTokenSource(ct); try { _logger.Log(LogLevel.Info, "Starting BotPages application..."); var adapterTasks = new List(); foreach (var adapter in _adapterFactory.AllAdapters) { var adapterTask = adapter.StartAdapterAsync(HandleUpdateAsync, _commands.Commands, CancellationToken); adapterTasks.Add(adapterTask); } await Task.WhenAll(adapterTasks); _isRunning = true; _logger.Log(LogLevel.Info, "BotPages application started successfully."); } catch (Exception ex) { _internalCts?.Dispose(); _internalCts = null; _logger.Log(LogLevel.Critical, "Failed to start BotPages application.", ex); throw; } _isRunning = true; } /// /// Остановить работу приложения. /// public void Stop() { if (!_isRunning || _disposed) return; _logger.Log(LogLevel.Info, "Stopping BotPages application..."); _internalCts?.Cancel(); } /// /// Синхронно ожидать завершения работы приложения. /// /// Если приложение не запущено public void Wait() { if (!_isRunning) throw new InvalidOperationException("Application is not running."); try { // Ждем, пока токен не будет отменен WaitHandle.WaitAny([_internalCts.Token.WaitHandle]); } catch (AggregateException ex) when (ex.InnerException is OperationCanceledException) { // Игнорируем отмену операции } finally { _isRunning = false; } } /// /// Асинхронно ожидать завершения работы приложения. /// Ожидает, пока не будет вызван или внешний не будет отменен. /// public async Task WaitAsync() { if (!_isRunning || _internalCts == null) return; try { var tcs = new TaskCompletionSource(); using (_internalCts.Token.Register(() => tcs.TrySetResult(true))) { await tcs.Task; } } catch (ObjectDisposedException) { // Токен уже освобожден } finally { _isRunning = false; } } /// public void Dispose() { DisposeAsync().AsTask().Wait(); } /// public async ValueTask DisposeAsync() { if (_disposed) return; _disposed = true; // Останавливаем приложение, если оно запущено if (_isRunning) { Stop(); } _internalCts?.Dispose(); foreach (var adapter in _adapterFactory.AllAdapters) { try { if (adapter is IAsyncDisposable asyncDisposable) { await asyncDisposable.DisposeAsync(); } else if (adapter is IDisposable disposable) { disposable.Dispose(); } } catch (Exception ex) { _logger.Log(LogLevel.Warn, $"Error disposing adapter {adapter.Id}: {ex.Message}"); } } } }