Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.9" />
<PackageVersion Include="Microsoft.FeatureManagement.AspNetCore" Version="4.5.0" />
<PackageVersion Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageVersion Include="System.Threading.RateLimiting" Version="10.0.9" />
<PackageVersion Include="Telegram.Bot" Version="22.10.1" />
Expand Down
46 changes: 28 additions & 18 deletions TgBotFrame.Commands.Help/HelpCommandController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Collections.Frozen;
using System.Reflection;
using System.Reflection;
using System.Resources;
using System.Text;
using Microsoft.FeatureManagement;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups;
Expand All @@ -14,7 +14,10 @@
namespace TgBotFrame.Commands.Help;

[CommandController(nameof(Help))]
public class HelpCommandController(ITelegramBotClient botClient, CommandExplorerService commandExplorer)
public class HelpCommandController(
ITelegramBotClient botClient,
CommandExplorerService commandExplorer,
IVariantFeatureManager? featureManager = null)
: CommandControllerBase
{
[Command(nameof(Help))]
Expand Down Expand Up @@ -56,8 +59,9 @@ public async Task HelpList()
text.AppendLine();
text.AppendLine();

IEnumerable<InlineKeyboardButton[]> buttons = commandExplorer.Commands.Values
.SelectMany(x => x.Keys)

IAsyncEnumerable<InlineKeyboardButton[]> buttons = commandExplorer.GetCommands(featureManager)
.SelectMany(x => x.Value.Select(y => y.Key))
.Select(x => x.DeclaringType)
.Select(x => (x?.Assembly,
x?.GetCustomAttribute<CommandControllerAttribute>()?.CategoryKey))
Expand Down Expand Up @@ -92,7 +96,7 @@ await botClient.SendMessage(
Context.GetUserId()!,
Resources.ResourceManager.GetString(nameof(HelpCommandController_HelpList_Description),
Context.GetCultureInfo())!,
replyMarkup: new InlineKeyboardMarkup(buttons)
replyMarkup: new InlineKeyboardMarkup(await buttons.ToArrayAsync())
).ConfigureAwait(false);
}

Expand All @@ -102,8 +106,8 @@ await botClient.SendMessage(
[Command(nameof(HelpCategory))]
public async Task HelpCategory(string category)
{
if (!commandExplorer.CategoriesCommandsNames.TryGetValue(category,
out (Assembly assembly, FrozenSet<string> commands) commands))
string[] commands = await commandExplorer.GetCategoryCommandsNames(featureManager, category).ToArrayAsync();
if (commands.Length == 0)
{
await botClient.SendMessage(
Context.GetUserId()!,
Expand All @@ -112,16 +116,22 @@ await botClient.SendMessage(
return;
}

IEnumerable<InlineKeyboardButton[]> buttons = commands.commands.Select(x => new[]
IEnumerable<InlineKeyboardButton[]> buttons = commands.Select(x => new[]
{ InlineKeyboardButton.WithCallbackData(x, $@"/{nameof(HelpCommand)} {x}") });

string text = (category.Length == 0
? Resources.ResourceManager.GetString(
string text;
if (category.Length == 0)
{
text = Resources.ResourceManager.GetString(
nameof(HelpCommandController_HelpCategory_NoCategory),
Context.GetCultureInfo())
: GetResourceManager(commands.assembly)?.GetString(
CATEGORY_DESCRIPTION_PREFIX + category,
Context.GetCultureInfo())) ?? category;
Context.GetCultureInfo()) ?? category;
}
else
{
text = commandExplorer.GetAssembliesForCategory(category).Select(x => GetResourceManager(x)
?.GetString(CATEGORY_DESCRIPTION_PREFIX + category,
Context.GetCultureInfo())).FirstOrDefault(x => !string.IsNullOrEmpty(x)) ?? category;
}

await botClient.SendMessage(
Context.GetUserId()!,
Expand All @@ -132,9 +142,9 @@ await botClient.SendMessage(
[Command(nameof(HelpCommand))]
public async Task HelpCommand(string command)
{
if (!commandExplorer.Commands.TryGetValue(command,
out FrozenDictionary<MethodInfo, ParameterInfo[]>? methods)
|| methods.Count == 0)
Dictionary<MethodInfo, ParameterInfo[]> methods =
await commandExplorer.GetCommand(featureManager, command).ToDictionaryAsync();
if (methods.Count == 0)
{
await botClient.SendMessage(
Context.GetUserId()!,
Expand Down
16 changes: 10 additions & 6 deletions TgBotFrame.Commands/Middleware/CommandRouterMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System.Collections.Frozen;
using System.Globalization;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.FeatureManagement;
using Telegram.Bot;
using TgBotFrame.Commands.Extensions;
using TgBotFrame.Commands.Properties;
Expand All @@ -15,7 +15,8 @@ public sealed class CommandRouterMiddleware(
CommandExplorerService commandExplorerService,
IServiceScopeFactory scopeFactory,
ITelegramBotClient botClient,
ILogger<CommandRouterMiddleware> logger) : FrameMiddleware
ILogger<CommandRouterMiddleware> logger,
IVariantFeatureManager? featureManager = null) : FrameMiddleware
{
public const string COMMAND_CONTROLLER_KEY = "CommandController";
public const string COMMAND_METHOD_KEY = "CommandMethod";
Expand All @@ -38,8 +39,11 @@ public override async Task InvokeAsync(Update update, FrameContext context, Canc
ExceptedName = context.GetBotUsername();
CommandArgumentsRaw = context.GetCommandArgsRaw();

if (!commandExplorerService.Commands.TryGetValue(CommandKey,
out FrozenDictionary<MethodInfo, ParameterInfo[]>? allMethods) || allMethods.Count == 0)

Dictionary<MethodInfo, ParameterInfo[]> allMethods = await commandExplorerService
.GetCommand(featureManager, CommandKey).ToDictionaryAsync(cancellationToken: ct);

if (allMethods.Count == 0)
{
await SendCommandNotFound(update, context, ct).ConfigureAwait(false);
await Next(update, context, ct).ConfigureAwait(false);
Expand Down Expand Up @@ -180,7 +184,7 @@ await botClient.SendMessage(update.Message!.Chat,
}

private (MethodInfo?, int invalidArgIndex) GetMethod(
in FrozenDictionary<MethodInfo, ParameterInfo[]> allMethods, out object?[] args)
in IDictionary<MethodInfo, ParameterInfo[]> allMethods, out object?[] args)
{
args = [];
KeyValuePair<MethodInfo, ParameterInfo[]> method;
Expand Down
117 changes: 92 additions & 25 deletions TgBotFrame.Commands/Services/CommandExplorerService.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,104 @@
using System.Collections.Frozen;
using Microsoft.Extensions.Logging;
using Microsoft.FeatureManagement;
using System.Collections.Frozen;
using System.Reflection;
using Microsoft.Extensions.Logging;
using TgBotFrame.Commands.Attributes;
using TgBotFrame.Services;

namespace TgBotFrame.Commands.Services;

public sealed class CommandExplorerService(ILogger<CommandExplorerService> logger)
{
public FrozenDictionary<string, FrozenDictionary<MethodInfo, ParameterInfo[]>> Commands { get; private set; } =
private FrozenDictionary<string, FrozenDictionary<MethodInfo, ParameterInfo[]>> COMMANDS =
FrozenDictionary<string, FrozenDictionary<MethodInfo, ParameterInfo[]>>.Empty;
private FrozenDictionary<string, (FrozenDictionary<string, MethodInfo[]> methods, Assembly[] assemblies)> CATEGORIES =
FrozenDictionary<string, (FrozenDictionary<string, MethodInfo[]> methods, Assembly[] assemblies)>.Empty;

public FrozenDictionary<string, (Assembly, FrozenSet<string>)> CategoriesCommandsNames { get; private set; } =
FrozenDictionary<string, (Assembly, FrozenSet<string>)>.Empty;
public async IAsyncEnumerable<KeyValuePair<MethodInfo, ParameterInfo[]>> GetCommand(IVariantFeatureManager? featureManager, string commandName)
{
if (!COMMANDS.TryGetValue(commandName, out FrozenDictionary<MethodInfo, ParameterInfo[]>? commands))
{
yield break;
}

if (featureManager is null)
{
foreach (KeyValuePair<MethodInfo, ParameterInfo[]> method in commands)
{
yield return method;
}
}
else
{
foreach (KeyValuePair<MethodInfo, ParameterInfo[]> method in commands)
{
if (await BotService.IsFeatureEnabled(featureManager, method.Key).ConfigureAwait(false))
yield return method;
}
}
}

public async IAsyncEnumerable<KeyValuePair<string, IAsyncEnumerable<KeyValuePair<MethodInfo, ParameterInfo[]>>>> GetCommands(IVariantFeatureManager? featureManager)
{
foreach (KeyValuePair<string, FrozenDictionary<MethodInfo, ParameterInfo[]>> commandName in COMMANDS)
{
yield return new(commandName.Key, GetCommand(featureManager, commandName.Key));
}
}



public IReadOnlyCollection<Assembly> GetAssembliesForCategory(in string category)
{
return CATEGORIES.TryGetValue(category, out (FrozenDictionary<string, MethodInfo[]> methods, Assembly[] assemblies) value) ? value.assemblies : [];
}

public async IAsyncEnumerable<string> GetCategoryCommandsNames(IVariantFeatureManager? featureManager, string category)
{
if (!CATEGORIES.TryGetValue(category, out (FrozenDictionary<string, MethodInfo[]> methods, Assembly[] assemblies) methods))
{
yield break;
}

foreach (KeyValuePair<string, MethodInfo[]> method in methods.methods)
{
foreach (MethodInfo overload in method.Value)
{
if (!await BotService.IsFeatureEnabled(featureManager, overload))
{
continue;
}

yield return method.Key;
break;
}
}
}

public void FillControllers(in IEnumerable<Type> commandControllers)
{
Dictionary<string, List<MethodInfo>> controllers = [];
Dictionary<string, List<string>> categories = [];
Dictionary<string, Assembly> assemblies = [];
Dictionary<string, List<MethodInfo>> commands = [];
Dictionary<string, (Dictionary<string, List<MethodInfo>> methods, List<Assembly> assemblies)> categories =
[];

foreach (Type type in commandControllers.Distinct())
{
CommandControllerAttribute? controllerAttribute = type.GetCustomAttribute<CommandControllerAttribute>();
if (controllerAttribute is null)
{
continue;
}
Assembly assembly = type.Assembly;

if (!categories.TryGetValue(controllerAttribute.CategoryKey, out List<string>? categoryMethods))

if (categories.TryGetValue(controllerAttribute.CategoryKey, out (Dictionary<string, List<MethodInfo>> methods, List<Assembly> assemblies) categoryInfo))
{
if (!categoryInfo.assemblies.Contains(assembly)) categoryInfo.assemblies.Add(assembly);
}
else
{
categoryMethods = [];
categories.Add(controllerAttribute.CategoryKey, categoryMethods);
assemblies.Add(controllerAttribute.CategoryKey, type.Assembly);
categories.Add(controllerAttribute.CategoryKey, ([], [assembly]));
categoryInfo = categories[controllerAttribute.CategoryKey];
}

foreach (MethodInfo methodInfo in type.GetMethods())
Expand All @@ -41,32 +109,31 @@ public void FillControllers(in IEnumerable<Type> commandControllers)
continue;
}

string name = attribute.Name;
categoryMethods.Add(name);
if (controllers.TryGetValue(name, out List<MethodInfo>? methods))
if (!categoryInfo.methods.TryAdd(attribute.Name, [methodInfo]))
{
if (!methods.Contains(methodInfo))
{
methods.Add(methodInfo);
}
categoryInfo.methods[attribute.Name].Add(methodInfo);
}
else

if (!commands.TryAdd(attribute.Name, [methodInfo]))
{
controllers.Add(name, [methodInfo]);
var overloads = commands[attribute.Name];
if (!overloads.Contains(methodInfo)) overloads.Add(methodInfo);
}
}

}

Commands = controllers.ToFrozenDictionary(
COMMANDS = commands.ToFrozenDictionary(
x => x.Key,
x => x.Value.ToFrozenDictionary(
y => y,
y => y.GetParameters()),
StringComparer.OrdinalIgnoreCase);
CategoriesCommandsNames = categories.ToFrozenDictionary(x => x.Key,
x => (assemblies[x.Key], x.Value.Distinct(StringComparer.OrdinalIgnoreCase).ToFrozenSet()));
CATEGORIES = categories.ToFrozenDictionary(x => x.Key,
x => (x.Value.methods.ToFrozenDictionary(y => y.Key, y => y.Value.ToArray()),
x.Value.assemblies.ToArray()));

logger.LogInformation(@"{all_count:D} overload(s) from {count:D} command(s) loaded",
Commands.Sum(x => x.Value.Count), Commands.Count);
COMMANDS.Sum(x => x.Value.Count), COMMANDS.Count);
}
}
14 changes: 10 additions & 4 deletions TgBotFrame.Example/ExtendedHelpController.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
using Telegram.Bot;
using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.Mvc;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;
using TgBotFrame.Commands;
using TgBotFrame.Commands.Attributes;
using TgBotFrame.Commands.Extensions;
using TgBotFrame.Commands.Help;
using TgBotFrame.Commands.Services;

namespace TgBotFrame.Example;

[CommandController("Help")]
public class ExtendedHelpController(ITelegramBotClient botClient, CommandExplorerService commandExplorer)
: HelpCommandController(botClient, commandExplorer)
public class ExtendedHelpController(
ITelegramBotClient botClient,
CommandExplorerService commandExplorer,

Check warning on line 15 in TgBotFrame.Example/ExtendedHelpController.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'commandExplorer' is unread.

Check warning on line 15 in TgBotFrame.Example/ExtendedHelpController.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'commandExplorer' is unread.
IVariantFeatureManager? featureManager = null)

Check warning on line 16 in TgBotFrame.Example/ExtendedHelpController.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'featureManager' is unread.

Check warning on line 16 in TgBotFrame.Example/ExtendedHelpController.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'featureManager' is unread.
: CommandControllerBase //: HelpCommandController(botClient, commandExplorer, featureManager)
{
private readonly ITelegramBotClient _botClient = botClient;

[Command(nameof(EchoDouble))]
[FeatureGate("EchoDoubleFeature")]
public async Task EchoDouble(string text)
{
int? messageId = Context.GetMessageId();
Expand Down
3 changes: 3 additions & 0 deletions TgBotFrame.Example/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.FeatureManagement;
using OpenTelemetry.Metrics;
using Telegram.Bot;
using TgBotFrame.Commands.Authorization.Extensions;
Expand Down Expand Up @@ -39,6 +40,8 @@
const string sqliteConnectionString = "Data Source=../data/sqlite/example.sqlite";

string? tgToken = builder.Configuration.GetConnectionString("Telegram");
builder.Services.AddFeatureManagement();

builder.Services.AddTelegramHttpClient();
builder.Services.AddSingleton<ITelegramBotClient, TelegramBotClient>(provider =>
{
Expand Down
38 changes: 23 additions & 15 deletions TgBotFrame.Example/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"Telegram": ""
},
"RateLimit": {
"Limit": 1,
"Interval": "00:00:01"
},
"AllowedHosts": "*"
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"Telegram": ""
},
"RateLimit": {
"Limit": 1,
"Interval": "00:00:01"
},
"AllowedHosts": "*",
"feature_management": {
"feature_flags": [
{
"id": "EchoDoubleFeature",
"enabled": false
}
]
}
}
Loading
Loading