* Changed public API overview generator behaviour, now working only in DEBUG builds

* Fixed wrong LeveldDebug method calls after moving logic from providers to router
* Added independent "IndentFlags" property to inner debugger class
* Fixed debug logging in few places
* Removed "ICollectingOptions" and merged it with new options abstract "ITelegratorOptions"
* Added WebHook version of hosting class
This commit is contained in:
2025-07-28 20:35:48 +04:00
parent 4e53337496
commit 5320c9ec20
47 changed files with 873 additions and 148 deletions
@@ -1,8 +1,14 @@
using Telegrator.Hosting.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Telegrator.Hosting.Components;
namespace Telegrator.Hosting.Web.Components
{
public interface ITelegramBotWebHost : ITelegramBotHost//, IEndpointRouteBuilder
/// <summary>
/// Interface for Telegram bot hosts with Webhook update receiving.
/// Combines wbe application capabilities with reactive Telegram bot functionality.
/// </summary>
public interface ITelegramBotWebHost : ITelegramBotHost, IEndpointRouteBuilder, IApplicationBuilder, IAsyncDisposable
{
}
@@ -0,0 +1,10 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0290")]
[assembly: SuppressMessage("Style", "IDE0090")]
[assembly: SuppressMessage("Usage", "CA2254")]
@@ -1,49 +1,93 @@
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using System.Text.Json;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegrator.Hosting.Web.Components;
using Telegrator.MadiatorCore;
namespace Telegrator.Hosting.Web.Polling
{
/// <summary>
/// Service for receiving updates for Hosted telegram bots via Webhooks
/// </summary>
public class HostedUpdateWebhooker : IHostedService
{
private const string SecretTokenHeader = "X-Telegram-Bot-Api-Secret-Token";
private readonly ITelegramBotWebHost _botHost;
private readonly ITelegramBotClient _botClient;
private readonly IUpdateRouter _updateRouter;
private readonly TelegramBotWebOptions _options;
/// <summary>
/// Initiallizes new instance of <see cref="HostedUpdateWebhooker"/>
/// </summary>
/// <param name="botHost"></param>
/// <param name="botClient"></param>
/// <param name="updateRouter"></param>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
public HostedUpdateWebhooker(ITelegramBotWebHost botHost, ITelegramBotClient botClient, IUpdateRouter updateRouter, IOptions<TelegramBotWebOptions> options)
{
if (string.IsNullOrEmpty(options.Value.WebhookUri))
throw new ArgumentNullException(nameof(options), "Option \"WebhookUrl\" must be set to subscribe for update recieving");
if (string.IsNullOrEmpty(options.Value.WebhookPattern))
throw new ArgumentNullException(nameof(options), "Option \"WebhookPattern\" must be set to subscribe for update recieving");
_botHost = botHost;
_botClient = botClient;
_updateRouter = updateRouter;
_options = options.Value;
}
/// <inheritdoc/>
public Task StartAsync(CancellationToken cancellationToken)
{
string pattern = new UriBuilder(_options.WebhookUri).Path;
_botHost.MapPost(pattern, (Delegate)ReceiveUpdate);
_botClient.SetWebhook(
url: _options.WebhookUri,
maxConnections: _options.MaxConnections,
allowedUpdates: _botHost.UpdateRouter.HandlersProvider.AllowedTypes,
dropPendingUpdates: _options.DropPendingUpdates,
cancellationToken: cancellationToken);
cancellationToken: cancellationToken)
.Wait(cancellationToken);
//botHost.MapGet(_options.WebhookPattern, async (Update update) => await _updateRouter.HandleUpdateAsync(_botClient, update, cancellationToken));
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task StopAsync(CancellationToken cancellationToken)
{
_botClient.DeleteWebhook(_options.DropPendingUpdates, cancellationToken);
return Task.CompletedTask;
}
private async Task<IResult> ReceiveUpdate(HttpContext ctx)
{
if (_options.SecretToken != null)
{
if (!ctx.Request.Headers.TryGetValue(SecretTokenHeader, out StringValues strings))
return Results.BadRequest();
string? secret = strings.SingleOrDefault();
if (secret == null)
return Results.BadRequest();
if (_options.SecretToken != secret)
return Results.StatusCode(401);
}
Update? update = await JsonSerializer.DeserializeAsync<Update>(ctx.Request.Body, JsonBotAPI.Options, ctx.RequestAborted);
if (update is not { Id: > 0 })
return Results.BadRequest();
await _updateRouter.HandleUpdateAsync(_botClient, update, ctx.RequestAborted);
return Results.Ok();
}
}
}
+84
View File
@@ -0,0 +1,84 @@
# Telegrator.Hosting.Web
**Telegrator.Hosting.Web** is an extension for the Telegrator framework that enables seamless integration with ASP.NET Core and webhook-based Telegram bots. It is designed for scalable, production-ready web applications.
---
## Features
- ASP.NET Core integration for webhook-based bots
- Automatic handler discovery and registration
- Strongly-typed configuration via `appsettings.json` and environment variables
- Dependency injection and middleware support
- Graceful startup/shutdown and lifecycle management
- Advanced error handling and logging
- Supports all Telegrator handler/filter/state features
---
## Requirements
- .NET 8.0 or later
- ASP.NET Core
---
## Installation
```shell
dotnet add package Telegrator.Hosting.Web
```
---
## Quick Start Example
**Program.cs (ASP.NET Core):**
```csharp
using Telegrator.Hosting;
using Telegrator.Hosting.Web;
// Creating builder
TelegramBotWebHostBuilder builder = TelegramBotWebHost.CreateBuilder(new TelegramBotWebOptions()
{
Args = args,
WebhookUri = "https://you-public-host.ru/bot"
ExceptIntersectingCommandAliases = true
});
// Register handlers
builder.Handlers.CollectHandlersAssemblyWide();
builder.Services.AddHandlersFromAssembly(typeof(Program).Assembly);
// Register your services
builder.Services.AddSingleton<IMyService, MyService>();
// Building and running application
TelegramBotWebHost telegramBot = builder.Build();
telegramBot.SetBotCommands();
telegramBot.Run();
```
---
## Configuration (appsettings.json)
```json
{
"TelegramBotClientOptions": {
"Token": "YOUR_BOT_TOKEN"
}
}
```
- `TelegramBotClientOptions`: Bot token and client settings
---
## Documentation
- [Telegrator Main Docs](https://github.com/Rikitav/Telegrator)
- [Getting Started Guide](https://github.com/Rikitav/Telegrator/wiki/Getting-started)
- [Annotation Overview](https://github.com/Rikitav/Telegrator/wiki/Annotation-overview)
---
## License
GPLv3
+188 -2
View File
@@ -1,10 +1,196 @@
using Telegrator.Hosting.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Text;
using Telegram.Bot.Types.Enums;
using Telegrator.Hosting.Components;
using Telegrator.Hosting.Providers;
using Telegrator.Hosting.Web.Components;
using Telegrator.MadiatorCore;
using Telegrator.MadiatorCore.Descriptors;
namespace Telegrator.Hosting.Web
{
public class TelegramBotWebHost //: ITelegramBotWebHost
/// <summary>
/// Represents a web hosted telegram bot
/// </summary>
public class TelegramBotWebHost : ITelegramBotWebHost
{
private readonly WebApplication _innerApp;
private readonly IUpdateRouter _updateRouter;
private readonly ILogger<TelegramBotHost> _logger;
private bool _disposed;
/// <inheritdoc/>
public IServiceProvider Services => _innerApp.Services;
/// <inheritdoc/>
public IUpdateRouter UpdateRouter => _updateRouter;
/// <inheritdoc/>
public ICollection<EndpointDataSource> DataSources => ((IEndpointRouteBuilder)_innerApp).DataSources;
/// <summary>
/// Allows consumers to be notified of application lifetime events.
/// </summary>
public IHostApplicationLifetime Lifetime => _innerApp.Lifetime;
/// <summary>
/// This application's logger
/// </summary>
public ILogger<TelegramBotHost> Logger => _logger;
// Private interface fields
IServiceProvider IEndpointRouteBuilder.ServiceProvider => Services;
IServiceProvider IApplicationBuilder.ApplicationServices { get => Services; set => throw new NotImplementedException(); }
IFeatureCollection IApplicationBuilder.ServerFeatures => ((IApplicationBuilder)_innerApp).ServerFeatures;
IDictionary<string, object?> IApplicationBuilder.Properties => ((IApplicationBuilder)_innerApp).Properties;
internal TelegramBotWebHost(WebApplicationBuilder webApplicationBuilder, HostHandlersCollection handlers)
{
RegisterHostServices(webApplicationBuilder, handlers);
_innerApp = webApplicationBuilder.Build();
_updateRouter = Services.GetRequiredService<IUpdateRouter>();
_logger = Services.GetRequiredService<ILogger<TelegramBotHost>>();
LogHandlers(handlers);
}
/// <summary>
/// Creates new <see cref="TelegramBotHostBuilder"/> with default services and webhook update receiving scheme
/// </summary>
/// <returns></returns>
public static TelegramBotWebHostBuilder CreateBuilder(TelegramBotWebOptions settings)
{
ArgumentNullException.ThrowIfNull(settings, nameof(settings));
WebApplicationBuilder innerApp = WebApplication.CreateBuilder(settings.ToWebApplicationOptions());
TelegramBotWebHostBuilder builder = new TelegramBotWebHostBuilder(innerApp, settings);
builder.Services.AddTelegramBotHostDefaults();
builder.Services.AddTelegramWebhook();
return builder;
}
/// <summary>
/// Creates new SLIM <see cref="TelegramBotHostBuilder"/> with default services and webhook update receiving scheme
/// </summary>
/// <returns></returns>
public static TelegramBotWebHostBuilder CreateSlimBuilder(TelegramBotWebOptions settings)
{
ArgumentNullException.ThrowIfNull(settings, nameof(settings));
WebApplicationBuilder innerApp = WebApplication.CreateSlimBuilder(settings.ToWebApplicationOptions());
TelegramBotWebHostBuilder builder = new TelegramBotWebHostBuilder(innerApp, settings);
builder.Services.AddTelegramBotHostDefaults();
builder.Services.AddTelegramWebhook();
return builder;
}
/// <summary>
/// Creates new EMPTY <see cref="TelegramBotHostBuilder"/> WITHOUT any services or update receiving schemes
/// </summary>
/// <returns></returns>
public static TelegramBotWebHostBuilder CreateEmptyBuilder(TelegramBotWebOptions settings)
{
ArgumentNullException.ThrowIfNull(settings, nameof(settings));
WebApplicationBuilder innerApp = WebApplication.CreateEmptyBuilder(settings.ToWebApplicationOptions());
return new TelegramBotWebHostBuilder(innerApp, settings);
}
/// <inheritdoc/>
public async Task StartAsync(CancellationToken cancellationToken = default)
{
await _innerApp.StartAsync(cancellationToken);
}
/// <inheritdoc/>
public async Task StopAsync(CancellationToken cancellationToken = default)
{
await _innerApp.StopAsync(cancellationToken);
}
/// <inheritdoc/>
public IApplicationBuilder CreateApplicationBuilder()
=> ((IEndpointRouteBuilder)_innerApp).CreateApplicationBuilder();
/// <inheritdoc/>
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
=> _innerApp.Use(middleware);
/// <inheritdoc/>
public IApplicationBuilder New()
=> ((IApplicationBuilder)_innerApp).New();
/// <inheritdoc/>
public RequestDelegate Build()
=> ((IApplicationBuilder)_innerApp).Build();
/// <summary>
/// Disposes the host.
/// </summary>
public async ValueTask DisposeAsync()
{
if (_disposed)
return;
await _innerApp.DisposeAsync();
GC.SuppressFinalize(this);
_disposed = true;
}
/// <summary>
/// Disposes the host.
/// </summary>
public void Dispose()
{
if (_disposed)
return;
// Sorry for this, i really dont know how to handle such cases
ValueTask disposeTask = _innerApp.DisposeAsync();
while (!disposeTask.IsCompleted)
Thread.Sleep(100);
GC.SuppressFinalize(this);
_disposed = true;
}
private void LogHandlers(HostHandlersCollection handlers)
{
StringBuilder logBuilder = new StringBuilder("Registered handlers : ");
if (!handlers.Keys.Any())
throw new Exception();
foreach (UpdateType updateType in handlers.Keys)
{
HandlerDescriptorList descriptors = handlers[updateType];
logBuilder.Append("\n\tUpdateType." + updateType + " :");
foreach (HandlerDescriptor descriptor in descriptors.Reverse())
{
logBuilder.AppendFormat("\n\t* {0} - {1}",
descriptor.Indexer.ToString(),
descriptor.ToString());
}
}
Logger.LogInformation(logBuilder.ToString());
}
private void RegisterHostServices(WebApplicationBuilder hostApplicationBuilder, HostHandlersCollection handlers)
{
//hostApplicationBuilder.Services.RemoveAll<IHost>();
//hostApplicationBuilder.Services.AddSingleton<IHost>(this);
hostApplicationBuilder.Services.AddSingleton<ITelegramBotHost>(this);
hostApplicationBuilder.Services.AddSingleton<ITelegramBotWebHost>(this);
hostApplicationBuilder.Services.AddSingleton<ITelegratorBot>(this);
hostApplicationBuilder.Services.AddSingleton<IHandlersCollection>(handlers);
}
}
}
@@ -1,10 +1,73 @@
using Telegrator.Hosting.Components;
using Telegrator.Hosting.Web.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegrator.Hosting.Components;
using Telegrator.Hosting.Configuration;
using Telegrator.Hosting.Providers;
using Telegrator.MadiatorCore;
#pragma warning disable IDE0001
namespace Telegrator.Hosting.Web
{
public class TelegramBotWebHostBuilder //: ITelegramBotHostBuilder
/// <summary>
/// Represents a web hosted telegram bots and services builder that helps manage configuration, logging, lifetime, and more.
/// </summary>
public class TelegramBotWebHostBuilder : ITelegramBotHostBuilder
{
private readonly WebApplicationBuilder _innerBuilder;
private readonly TelegramBotWebOptions _settings;
private readonly HostHandlersCollection _handlers;
/// <inheritdoc/>
public IHandlersCollection Handlers => _handlers;
/// <inheritdoc/>
public IConfigurationManager Configuration => _innerBuilder.Configuration;
/// <inheritdoc/>
public ILoggingBuilder Logging => _innerBuilder.Logging;
/// <inheritdoc/>
public IServiceCollection Services => _innerBuilder.Services;
/// <inheritdoc/>
public IHostEnvironment Environment => _innerBuilder.Environment;
internal TelegramBotWebHostBuilder(WebApplicationBuilder webApplicationBuilder, TelegramBotWebOptions settings)
{
_innerBuilder = webApplicationBuilder;
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_handlers = new HostHandlersCollection(Services, _settings);
Services.AddSingleton<IOptions<TelegramBotWebOptions>>(Options.Create(settings));
Services.Configure<TelegratorOptions>(Configuration.GetSection(nameof(TelegratorOptions)));
Services.Configure<TelegramBotClientOptions>(Configuration.GetSection(nameof(TelegramBotClientOptions)), new TelegramBotClientOptionsProxy());
}
/// <summary>
/// Builds the host.
/// </summary>
/// <returns></returns>
public TelegramBotWebHost Build()
{
foreach (PreBuildingRoutine preBuildRoutine in _handlers.PreBuilderRoutines)
{
try
{
preBuildRoutine.Invoke(this);
}
catch (NotImplementedException)
{
_ = 0xBAD + 0xC0DE;
}
}
return new TelegramBotWebHost(_innerBuilder, _handlers);
}
}
}
@@ -1,18 +1,58 @@
using Telegrator.Configuration;
using Microsoft.AspNetCore.Builder;
namespace Telegrator.Hosting.Web
{
public class TelegramBotWebOptions : TelegramBotOptions
/// <summary>
/// Configuration options for Telegram bot behavior and execution settings.
/// Controls various aspects of bot operation including concurrency, routing, webhook receiving, and execution policies.
/// </summary>
public class TelegramBotWebOptions : TelegratorOptions
{
/// <summary>
/// Gets or sets uri for webhook update receiving
/// Gets or sets HTTPS URL to send updates to. Use an empty string to remove webhook integration
/// </summary>
public required string WebhookUri { get; set; }
public required string WebhookPattern { get; set; }
/// <summary>
/// A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 1-256 characters.
/// Only characters A-Z, a-z, 0-9, _ and - are allowed.
/// The header is useful to ensure that the request comes from a webhook set by you.
/// </summary>
public string? SecretToken { get; set; }
public int MaxConnections { get; set; }
/// <summary>
/// The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to 40.
/// Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput.
/// </summary>
public int MaxConnections { get; set; } = 40;
/// <summary>
/// Pass true to drop all pending updates
/// </summary>
public bool DropPendingUpdates { get; set; }
/// <inheritdoc cref="WebApplicationOptions.Args"/>
public string[]? Args { get; init; }
/// <inheritdoc cref="WebApplicationOptions.EnvironmentName"/>
public string? EnvironmentName { get; init; }
/// <inheritdoc cref="WebApplicationOptions.ApplicationName"/>
public string? ApplicationName { get; init; }
/// <inheritdoc cref="WebApplicationOptions.ContentRootPath"/>
public string? ContentRootPath { get; init; }
/// <inheritdoc cref="WebApplicationOptions.WebRootPath"/>
public string? WebRootPath { get; init; }
internal WebApplicationOptions ToWebApplicationOptions() => new WebApplicationOptions()
{
ApplicationName = ApplicationName,
Args = Args,
ContentRootPath = ContentRootPath,
EnvironmentName = EnvironmentName,
WebRootPath = WebRootPath
};
}
}
@@ -4,6 +4,18 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageProjectUrl></PackageProjectUrl>
<Title>Telegrator : Telegram.Bot mediator framework</Title>
<PackageIcon>telegrator_nuget.png</PackageIcon>
<RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl>
<PackageTags>telegram;bot;mediator;attributes;aspect;hosting;host;framework;easy;simple;handlers</PackageTags>
<EnableNETAnalyzers>True</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Version>1.0.7</Version>
</PropertyGroup>
<ItemGroup>
@@ -11,6 +23,25 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="2.3.0" />
<None Include="..\LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\resources\telegrator_nuget.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Update="README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Telegram.Bot.AspNetCore" Version="22.5.0" />
</ItemGroup>
</Project>
+10
View File
@@ -1,12 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Telegram.Bot;
using Telegrator.Hosting.Web.Components;
using Telegrator.Hosting.Web.Polling;
namespace Telegrator.Hosting.Web
{
/// <summary>
/// Contains extensions for <see cref="IServiceCollection"/>
/// Provides method to configure <see cref="ITelegramBotWebHost"/>
/// </summary>
public static class ServicesCollectionExtensions
{
/// <summary>
/// Registers <see cref="ITelegramBotClient"/> service with <see cref="HostedUpdateWebhooker"/> to receive updates using webhook
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddTelegramWebhook(this IServiceCollection services)
{
services.AddHttpClient<ITelegramBotClient>("tgwebhook").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory);