diff --git a/BotPages.Core/Abstractions/IMessengerAdapter.cs b/BotPages.Core/Abstractions/IMessengerAdapter.cs
index 284bd95..db37aad 100644
--- a/BotPages.Core/Abstractions/IMessengerAdapter.cs
+++ b/BotPages.Core/Abstractions/IMessengerAdapter.cs
@@ -59,5 +59,5 @@ public interface IMessangerAdapterSetup : IMessengerAdapter
///
///
///
- Task StartAdapterAsync(Func onUpdate, CancellationToken ct);
+ Task StartAdapterAsync(Func onUpdate, List commands, CancellationToken ct);
}
\ No newline at end of file
diff --git a/BotPages.Core/BotPagesApp.cs b/BotPages.Core/BotPagesApp.cs
index 67be146..c060ae4 100644
--- a/BotPages.Core/BotPagesApp.cs
+++ b/BotPages.Core/BotPagesApp.cs
@@ -69,16 +69,34 @@ public sealed class BotPagesApp
///
public BotPagesApp MapCommand(string commandTemplate) where TPage : Page
{
- _commands.Map(commandTemplate);
+ _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, Func handler)
+ public BotPagesApp MapCommand(string template, CommandHandler handler)
{
- _commands.Map(template, 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;
}
@@ -142,7 +160,6 @@ public sealed class BotPagesApp
{
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;
}
@@ -237,7 +254,7 @@ public sealed class BotPagesApp
{
foreach (var adapter in _adapterFactory.Adapters)
{
- await adapter.Value.StartAdapterAsync(update => HandleUpdateAsync(update, cancellationToken), cancellationToken);
+ await adapter.Value.StartAdapterAsync(update => HandleUpdateAsync(update, cancellationToken), _commands.Commands, cancellationToken);
}
}
}
\ No newline at end of file
diff --git a/BotPages.Core/Routing/Command.cs b/BotPages.Core/Routing/Command.cs
new file mode 100644
index 0000000..04ec926
--- /dev/null
+++ b/BotPages.Core/Routing/Command.cs
@@ -0,0 +1,34 @@
+namespace BotPages.Core.Routing;
+
+using System.Text.RegularExpressions;
+
+///
+/// Команда действий. Например "/start"
+///
+public class Command
+{
+ ///
+ /// Название команды.
+ ///
+ public required string Name { get; init; }
+
+ ///
+ /// Шаблон команды.
+ ///
+ public required Regex Pattern { get; init; }
+
+ ///
+ /// Обработчик команды.
+ ///
+ public required CommandHandler Handler { get; init; }
+
+ ///
+ /// Описание команды.
+ ///
+ public string? Description { get; init; }
+
+ ///
+ /// Публичный? нужно ли регистрировать в боте.
+ ///
+ public required bool Publish { get; init; }
+}
diff --git a/BotPages.Core/Routing/CommandHandler.cs b/BotPages.Core/Routing/CommandHandler.cs
new file mode 100644
index 0000000..ea69ba7
--- /dev/null
+++ b/BotPages.Core/Routing/CommandHandler.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BotPages.Core.Routing;
+
+///
+/// Обработчик команды: получает контекст страницы, аргументы команды и токен отмены.
+///
+public delegate Task CommandHandler(PageContext context, IReadOnlyDictionary? args, CancellationToken cancellationToken);
diff --git a/BotPages.Core/Routing/CommandsRegistry.cs b/BotPages.Core/Routing/CommandsRegistry.cs
index 627f15f..a63d43e 100644
--- a/BotPages.Core/Routing/CommandsRegistry.cs
+++ b/BotPages.Core/Routing/CommandsRegistry.cs
@@ -2,30 +2,40 @@
using System.Text.RegularExpressions;
+
///
/// Реестр команд, доступных из любого места.
///
internal sealed class CommandsRegistry
{
- private readonly List<(Regex pattern, Func handler)> _commands = new();
+ private readonly List _commands = new();
+
+ public List Commands => _commands;
///
/// Зарегистрировать команду, ведущую на страницу.
///
- public CommandsRegistry Map(string commandTemplate) where TPage : Page
+ public CommandsRegistry Map(string commandTemplate, bool publish = false, string? description = null) where TPage : Page
{
var pattern = ToRegex(commandTemplate);
- _commands.Add((pattern, (ctx, ct) => ctx.Navigation.GoToAsync(ctx, ct)));
- return this;
+
+ return Map(commandTemplate, (ctx, args, ct) => ctx.Navigation.GoToAsync(ctx, ct), publish, description);
}
///
/// Зарегистрировать команду с кастомным обработчиком.
///
- public CommandsRegistry Map(string commandTemplate, Func handler)
+ public CommandsRegistry Map(string commandTemplate, CommandHandler handler, bool publish = false, string? description = null)
{
var pattern = ToRegex(commandTemplate);
- _commands.Add((pattern, handler));
+ _commands.Add(new Command()
+ {
+ Name = ToCommandName(commandTemplate),
+ Pattern = pattern,
+ Handler = handler,
+ Publish = publish,
+ Description = string.IsNullOrWhiteSpace(description) ? null : description
+ });
return this;
}
@@ -34,11 +44,19 @@ internal sealed class CommandsRegistry
///
public bool TryDispatch(PageContext ctx, string command, CancellationToken ct, out Task? task)
{
- foreach (var (pattern, handler) in _commands)
+ foreach (var cmd in _commands)
{
- if (pattern.IsMatch(command))
+ var match = cmd.Pattern.Match(command);
+ if (match.Success)
{
- task = handler(ctx, ct);
+ // Собираем аргументы
+ var args = cmd.Pattern.GetGroupNames()
+ .Where(n => n != "0")
+ .Select(n => new { n, v = match.Groups[n].Value })
+ .Where(x => !string.IsNullOrEmpty(x.v))
+ .ToDictionary(x => x.n, x => x.v);
+
+ task = cmd.Handler(ctx, args, ct);
return true;
}
}
@@ -56,4 +74,10 @@ internal sealed class CommandsRegistry
.Replace("{id?}", "(?\\S+)?") + "$";
return new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
+
+ private static string ToCommandName(string template)
+ {
+ // Простейшее преобразование шаблона: "/open {page} {id?}" -> "/open"
+ return template.Split(" ", StringSplitOptions.RemoveEmptyEntries).First().ToLowerInvariant();
+ }
}
diff --git a/BotPages.Telegram/TelegramAdapter.cs b/BotPages.Telegram/TelegramAdapter.cs
index 5851db6..bff4419 100644
--- a/BotPages.Telegram/TelegramAdapter.cs
+++ b/BotPages.Telegram/TelegramAdapter.cs
@@ -56,7 +56,7 @@ public sealed class TelegramAdapter : IMessangerAdapterSetup
///
/// Запустить polling для приема обновлений от Telegram.
///
- public async Task StartAdapterAsync(Func onUpdate, CancellationToken ct)
+ public async Task StartAdapterAsync(Func onUpdate, List commands, CancellationToken ct)
{
_client = new TelegramBotClient(_token);
@@ -77,6 +77,7 @@ public sealed class TelegramAdapter : IMessangerAdapterSetup
cancellationToken: ct
);
+ await _client.SetMyCommands(commands.Where(t => t.Publish).Select(t => new BotCommand(t.Name, t.Description ?? t.Name.TrimStart('/'))), cancellationToken: ct);
var me = await _client.GetMe();
_logger.Log(LogLevel.Info, $"{MessengerType} started: @{me.Username}");
diff --git a/Demo/Pages/FileSendPage.cs b/Demo/Pages/FileSendPage.cs
index 06861f0..c4e23b1 100644
--- a/Demo/Pages/FileSendPage.cs
+++ b/Demo/Pages/FileSendPage.cs
@@ -1,9 +1,11 @@
using BotPages.Core;
using BotPages.Core.Abstractions;
using BotPages.Core.Messaging;
+using BotPages.Core.Routing;
namespace Demo.Pages;
+[Route("FileSend")]
public sealed class FileSendPage : SingletonPage
{
public override Task OnEnter(PageContext ctx, CancellationToken ct)
diff --git a/Demo/Pages/WelcomePage.cs b/Demo/Pages/WelcomePage.cs
index 72c86fd..f9501e3 100644
--- a/Demo/Pages/WelcomePage.cs
+++ b/Demo/Pages/WelcomePage.cs
@@ -1,6 +1,7 @@
using BotPages.Core;
using BotPages.Core.Abstractions;
using BotPages.Core.Messaging;
+using BotPages.Core.Routing;
namespace Demo.Pages;
@@ -8,6 +9,7 @@ namespace Demo.Pages;
/// Стартовая страница демо‑бота.
/// Обычная страница с кнопками
///
+[Route("Welcome")]
public sealed class WelcomePage : SingletonPage
{
public override async Task OnEnter(PageContext ctx, CancellationToken ct)
diff --git a/Demo/Program.cs b/Demo/Program.cs
index f6b3e70..5cd2209 100644
--- a/Demo/Program.cs
+++ b/Demo/Program.cs
@@ -1,6 +1,7 @@
using BotPages.Core;
using BotPages.Core.Logging;
using BotPages.Core.Middleware;
+using BotPages.Core.Routing;
using BotPages.Core.Storage;
using BotPages.Telegram;
using Demo.Pages;
@@ -18,9 +19,26 @@ namespace Demo
var state = new InMemoryStateStorage();
using var cts = new CancellationTokenSource();
+ // Можно использовать команды для открытия страниц с роутингом
+ // /open Welcome
+ // /open FileSend
+ CommandHandler openHandler = async (ctx, args, ct) =>
+ {
+ if (args is null || !args.TryGetValue("page", out var pageName))
+ {
+ await ctx.SendTextAsync("Не указана страница для открытия.", ct: ct);
+ return;
+ }
+
+ // Навигация на страницу по имени
+ await ctx.Navigation.GoToAsync(pageName, ctx, ct);
+ };
+
var app = new BotPagesApp(state, logger)
.AddDefaultPage()
- .MapCommand("/start")
+ .MapCommand("/start", true, "Главная")
+ .MapCommand("/open {page}", openHandler, true, "открыть станицу /open {page}")
+ .AutoMapRoute()
.AddMiddleware(new ErrorHandlingMiddleware(logger))
.AddMiddleware(new LoggingMiddleware(logger))
.AddTelegramAdapter(token, "Telegram")