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
18 changes: 9 additions & 9 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<Project ToolsVersion="15.0">
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="10.0.9" />
<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="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.10" />
<PackageVersion Include="Microsoft.FeatureManagement.AspNetCore" Version="4.6.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" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="System.Threading.RateLimiting" Version="10.0.10" />
<PackageVersion Include="Telegram.Bot" Version="22.10.2.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
Expand Down
49 changes: 33 additions & 16 deletions TgBotFrame.Commands/Services/CommandExplorerService.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
using Microsoft.Extensions.Logging;
using Microsoft.FeatureManagement;
using System.Collections.Frozen;
using System.Collections.Frozen;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.FeatureManagement;
using TgBotFrame.Commands.Attributes;
using TgBotFrame.Services;

namespace TgBotFrame.Commands.Services;

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

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 async IAsyncEnumerable<KeyValuePair<MethodInfo, ParameterInfo[]>> GetCommand(IVariantFeatureManager? featureManager, string commandName)
public async IAsyncEnumerable<KeyValuePair<MethodInfo, ParameterInfo[]>> GetCommand(
IVariantFeatureManager? featureManager, string commandName)
{
if (!COMMANDS.TryGetValue(commandName, out FrozenDictionary<MethodInfo, ParameterInfo[]>? commands))
{
Expand All @@ -33,12 +36,15 @@ public async IAsyncEnumerable<KeyValuePair<MethodInfo, ParameterInfo[]>> GetComm
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)
public async IAsyncEnumerable<KeyValuePair<string, IAsyncEnumerable<KeyValuePair<MethodInfo, ParameterInfo[]>>>>
GetCommands(IVariantFeatureManager? featureManager)
{
foreach (KeyValuePair<string, FrozenDictionary<MethodInfo, ParameterInfo[]>> commandName in COMMANDS)
{
Expand All @@ -47,15 +53,19 @@ public async IAsyncEnumerable<KeyValuePair<string, IAsyncEnumerable<KeyValuePair
}



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

public async IAsyncEnumerable<string> GetCategoryCommandsNames(IVariantFeatureManager? featureManager, string category)
public async IAsyncEnumerable<string> GetCategoryCommandsNames(IVariantFeatureManager? featureManager,
string category)
{
if (!CATEGORIES.TryGetValue(category, out (FrozenDictionary<string, MethodInfo[]> methods, Assembly[] assemblies) methods))
if (!CATEGORIES.TryGetValue(category,
out (FrozenDictionary<string, MethodInfo[]> methods, Assembly[] assemblies) methods))
{
yield break;
}
Expand Down Expand Up @@ -88,12 +98,17 @@ public void FillControllers(in IEnumerable<Type> commandControllers)
{
continue;
}

Assembly assembly = type.Assembly;


if (categories.TryGetValue(controllerAttribute.CategoryKey, out (Dictionary<string, List<MethodInfo>> methods, List<Assembly> assemblies) categoryInfo))
if (categories.TryGetValue(controllerAttribute.CategoryKey,
out (Dictionary<string, List<MethodInfo>> methods, List<Assembly> assemblies) categoryInfo))
{
if (!categoryInfo.assemblies.Contains(assembly)) categoryInfo.assemblies.Add(assembly);
if (!categoryInfo.assemblies.Contains(assembly))
{
categoryInfo.assemblies.Add(assembly);
}
}
else
{
Expand All @@ -116,11 +131,13 @@ public void FillControllers(in IEnumerable<Type> commandControllers)

if (!commands.TryAdd(attribute.Name, [methodInfo]))
{
var overloads = commands[attribute.Name];
if (!overloads.Contains(methodInfo)) overloads.Add(methodInfo);
List<MethodInfo> overloads = commands[attribute.Name];
if (!overloads.Contains(methodInfo))
{
overloads.Add(methodInfo);
}
}
}

}

COMMANDS = commands.ToFrozenDictionary(
Expand Down
59 changes: 59 additions & 0 deletions TgBotFrame.Example/Controllers/WebhookController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Telegram.Bot.Types;
using TgBotFrame.Options;
using TgBotFrame.Services;

namespace TgBotFrame.Example.Controllers;

[ApiController]
[Route("tg")]
public class WebhookController(
BotService botService,
IOptions<TgBotOptions> options,
ILogger<WebhookController> logger,
IWebHostEnvironment webHostEnvironment) : ControllerBase
{
[HttpGet]
public ActionResult Get() => StatusCode(StatusCodes.Status405MethodNotAllowed);

[HttpPost]
public async Task<ActionResult> Post([FromBody] Update update, CancellationToken ct = default)
{
string? secretValue = options.Value.WebhookSecretToken;
if (secretValue is not null && secretValue != HttpContext.Request.Headers["X-Telegram-Bot-Api-Secret-Token"])
{
return Unauthorized();
}

try
{
await botService.HandleUpdateAsync(update, ct);
}
catch (TaskCanceledException e)
{
logger.LogWarning(e, "Webhook request was cancelled");
return Empty;
}
catch (Exception e)
{
logger.LogError(e, "Error handling update");
if (webHostEnvironment.IsDevelopment())
{
return Problem(
"Error handling update",
null,
StatusCodes.Status500InternalServerError,
e.Message,
e.GetType().Name);
}

return Problem(
"Error handling update",
null,
StatusCodes.Status500InternalServerError);
}

return Ok();
}
}
7 changes: 5 additions & 2 deletions TgBotFrame.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
using TgBotFrame.Commands.Start;
using TgBotFrame.Example;
using TgBotFrame.Injection;
using TgBotFrame.Options;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<TgBotOptions>(builder.Configuration.GetSection("TgBotOptions"));
builder.Services.AddOpenTelemetry().WithMetrics(providerBuilder =>
{
providerBuilder.AddPrometheusExporter();
Expand Down Expand Up @@ -66,6 +67,8 @@
commandsBuilder.TryAddControllers(Assembly.GetEntryAssembly()!);
});

builder.Services.AddControllers();

builder.Services.AddHealthChecks()
.AddUrlGroup(new Uri("https://api.telegram.org/"), HttpMethod.Head)
.AddSqlite(sqliteConnectionString);
Expand All @@ -83,5 +86,5 @@

app.MapPrometheusScrapingEndpoint();
app.UseHealthChecks("/health");

app.MapControllers();
await app.RunAsync().ConfigureAwait(false);
4 changes: 2 additions & 2 deletions TgBotFrame.Example/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "todos",
"applicationUrl": "http://localhost:5222",
"applicationUrl": "http://*:5222",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
6 changes: 3 additions & 3 deletions TgBotFrame.Example/TgBotFrame.Example.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.Sqlite" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.14.0-beta.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.16.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
</ItemGroup>

<ItemGroup>
Expand Down
7 changes: 7 additions & 0 deletions TgBotFrame.Example/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,12 @@
"enabled": false
}
]
},
"TgBotOptions": {
"BotMode": "Polling",
"WebhookUrl": "",
"WebhookSecretToken": "",
"WebhookMaxConnections": 30,
"AllowedUpdates": []
}
}
7 changes: 7 additions & 0 deletions TgBotFrame/Options/BotMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TgBotFrame.Options;

public enum BotMode
{
Polling,
Webhook,
}
12 changes: 12 additions & 0 deletions TgBotFrame/Options/TgBotOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Telegram.Bot.Types.Enums;

namespace TgBotFrame.Options;

public record TgBotOptions
{
public BotMode BotMode { get; init; } = BotMode.Polling;
public string? WebhookUrl { get; init; }
public string? WebhookSecretToken { get; init; }
public int? WebhookMaxConnections { get; init; }
public UpdateType[]? AllowedUpdates { get; init; } = Enum.GetValues<UpdateType>();
}
65 changes: 57 additions & 8 deletions TgBotFrame/Services/BotService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.Mvc;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types.Enums;
using TgBotFrame.Middleware;
using TgBotFrame.Options;

namespace TgBotFrame.Services;

Expand All @@ -19,14 +21,15 @@ namespace TgBotFrame.Services;
public class BotService(
ITelegramBotClient botClient,
ILogger<BotService> logger,
IOptions<TgBotOptions> options,
IServiceScopeFactory scopeFactory,
FrameMetricsService frameMetricsService) : BackgroundService, IUpdateHandler
{
public async Task HandleUpdateAsync(ITelegramBotClient _, Update update,
public Task HandleUpdateAsync(ITelegramBotClient _, Update update,
CancellationToken cancellationToken)
{
logger.LogDebug(@"Update {id} received", update.Id);
await RunMiddleware(update, cancellationToken).ConfigureAwait(false);
return RunMiddleware(update, cancellationToken);
}

public Task HandleErrorAsync(ITelegramBotClient _, Exception exception, HandleErrorSource source,
Expand All @@ -50,12 +53,54 @@ public Task HandleErrorAsync(ITelegramBotClient _, Exception exception, HandleEr
return Task.CompletedTask;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken) =>
await botClient.ReceiveAsync(this, new()
public Task HandleUpdateAsync(Update update,
CancellationToken cancellationToken)
{
logger.LogDebug(@"Update {id} received", update.Id);
return RunMiddleware(update, cancellationToken);
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
switch (options.Value.BotMode)
{
DropPendingUpdates = false,
AllowedUpdates = Enum.GetValues<UpdateType>(),
}, stoppingToken).ConfigureAwait(false);
case BotMode.Polling:
await botClient.ReceiveAsync(this, new()
{
DropPendingUpdates = false,
AllowedUpdates = (options.Value.AllowedUpdates?.Length ?? 0) == 0
? Enum.GetValues<UpdateType>()
: options.Value.AllowedUpdates,
}, stoppingToken).ConfigureAwait(false);
return;
case BotMode.Webhook when options.Value.WebhookUrl is not null:
try
{
await botClient.SetWebhook(
options.Value.WebhookUrl,
null,
null,
options.Value.WebhookMaxConnections,
(options.Value.AllowedUpdates?.Length ?? 0) == 0
? Enum.GetValues<UpdateType>()
: options.Value.AllowedUpdates,
false,
options.Value.WebhookSecretToken,
CancellationToken.None).ConfigureAwait(false);
await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken).ConfigureAwait(false);
}
finally
{
await botClient.DeleteWebhook(false, CancellationToken.None).ConfigureAwait(false);
}

return;
case BotMode.Webhook:
throw new ArgumentNullException(nameof(options.Value.WebhookUrl));
default:
throw new ArgumentOutOfRangeException(nameof(options));
}
}

private async Task RunMiddleware(Update update, CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -114,7 +159,11 @@ private static async ValueTask<bool> IsFeatureEnabled(IVariantFeatureManager fea
private static async ValueTask<bool> IsFeatureEnabled(IVariantFeatureManager? featureManager,
FeatureGateAttribute? attribute)
{
if (featureManager is null) return true;
if (featureManager is null)
{
return true;
}

switch (attribute?.RequirementType)
{
case RequirementType.Any:
Expand Down
Loading