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}");
}
}
}
}