* Added integration addon wit WTelegramBot (WIP)

* Added some extensions methods
* Refactored Result behaviour
* Added missing exception messages
* Removed telegrator-specific host builder (obsolete)
* Code cleanup and bug fixes
This commit is contained in:
gutii
2026-04-27 09:56:44 +04:00
parent 06a021de49
commit aba9cf4037
36 changed files with 814 additions and 990 deletions
@@ -0,0 +1,8 @@
// 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("Roslynator", "RCS1037")]
@@ -0,0 +1,35 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Threading;
using System.Threading.Tasks;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegrator.Core;
namespace Telegrator.Mediation;
//Hosting.WideBot
public class HostedWideBotUpdateReceiver(ILogger<HostedWideBotUpdateReceiver> logger, ITelegramBotClient botClient, IUpdateRouter updateRouter, IOptions<ReceiverOptions>? options) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (botClient is not WTelegramBotClient wideBotClient)
throw new Exception("Registered ITelegramBotClient was not a wide client (WTelegramBotClient)! Please, use `AddWideTelegrator` instead.");
if (options?.Value.DropPendingUpdates is true)
await wideBotClient.DropPendingUpdates();
logger.LogInformation("Starting receiving updates via MTProto");
// UIP (understanding in progress)
//_receiverOptions.AllowedUpdates = updateRouter.HandlersProvider.AllowedTypes.ToArray();
botClient.DeleteWebhook(options?.Value.DropPendingUpdates ?? false, cancellationToken: stoppingToken)
.ConfigureAwait(false).GetAwaiter().GetResult();
WideUpdateReceiver updateReceiver = new WideUpdateReceiver(wideBotClient);
await updateReceiver.ReceiveAsync(updateRouter, stoppingToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,50 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegrator.Core;
using WUpdate = WTelegram.Types.Update;
namespace Telegrator.Mediation;
public class WideUpdateReceiver(WTelegramBotClient client) : IUpdateReceiver
{
private readonly WTelegramBotClient _client = client;
private IUpdateHandler? _updateHandler = null;
private CancellationToken _cancellation = default;
public async Task ReceiveAsync(IUpdateHandler updateHandler, CancellationToken cancellationToken = default)
{
_updateHandler = updateHandler;
_cancellation = cancellationToken;
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
await using CancellationTokenRegistration registration = cancellationToken.Register(() => tcs.TrySetResult(null!));
try
{
_client.OnUpdate += OnUpdate;
await tcs.Task.ConfigureAwait(false);
}
finally
{
_client.OnUpdate -= OnUpdate;
}
}
private async Task OnUpdate(WUpdate update)
{
if (_updateHandler == null)
throw new Exception("Router not initialized (got null)");
try
{
await _updateHandler.HandleUpdateAsync(_client, update, _cancellation).ConfigureAwait(false);
}
catch (Exception ex)
{
await _updateHandler.HandleErrorAsync(_client, ex, HandleErrorSource.HandleUpdateError, _cancellation).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>Telegrator</RootNamespace>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WTelegramBot" Version="9.5.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Telegrator.Hosting\Telegrator.Hosting.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,76 @@
// Maybe later...
/*
using System;
using System.Net.Http;
using System.Threading;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegrator.Core;
using Telegrator.Logging;
using Telegrator.Mediation;
using Telegrator.Providers;
using Telegrator.States;
namespace Telegrator;
public class TelegratorWClient : WTelegramBotClient, ITelegratorBot, ICollectingProvider
{
private IUpdateRouter? _updateRouter = null;
public TelegratorOptions Options { get; }
public IHandlersCollection Handlers { get; }
public ITelegramBotInfo BotInfo { get; }
public IUpdateRouter UpdateRouter => _updateRouter ?? throw new InvalidOperationException("Router's not created yet. Invoke `StartReceiving` to initialize this property.");
public TelegratorWClient(WTelegramBotClientOptions wOptions, TelegratorOptions? telegratorOptions = null, HttpClient? httpClient = null, CancellationToken cancellationToken = default)
: base(wOptions, httpClient, cancellationToken)
{
Options = telegratorOptions ?? new TelegratorOptions();
Handlers = new HandlersCollection(default);
BotInfo = new TelegramBotInfo(GetMe(cancellationToken).Result);
}
public void StartReceiving(CancellationToken cancellationToken = default)
{
if (Options.GlobalCancellationToken == CancellationToken.None)
Options.GlobalCancellationToken = cancellationToken;
HandlersProvider handlerProvider = new HandlersProvider(Handlers, Options);
AwaitingProvider awaitingProvider = new AwaitingProvider(Options);
DefaultStateStorage stateStorage = new DefaultStateStorage();
_updateRouter = new UpdateRouter(handlerProvider, awaitingProvider, stateStorage, Options, BotInfo);
TelegratorLogging.LogInformation($"TelegratorW bot starting up - BotId: {BotInfo.User.Id}, Username: {BotInfo.User.Username}");
StartReceivingInternal(Options.GlobalCancellationToken);
}
private async void StartReceivingInternal(CancellationToken cancellationToken)
{
try
{
try
{
await new HostedWideBotUpdateReceiver(this)
.ReceiveAsync(UpdateRouter, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception exception)
{
await UpdateRouter
.HandleErrorAsync(this, exception, HandleErrorSource.FatalError, cancellationToken)
.ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// Cancelled
TelegratorLogging.LogInformation("Telegrator bot stopped (cancelled)");
}
}
}
*/
@@ -0,0 +1,181 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Threading;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegrator.Core;
using Telegrator.Core.Handlers;
using Telegrator.Hosting;
using Telegrator.Mediation;
using Telegrator.Providers;
using TLUpdate = TL.Update;
using WUpdate = WTelegram.Types.Update;
namespace Telegrator;
public static class HandlersExtensions
{
extension<TUpdate>(AbstractUpdateHandler<TUpdate> handler) where TUpdate : class
{
public WUpdate WUpdate
{
get
{
object? update = typeof(AbstractUpdateHandler<TUpdate>).GetField("HandlingUpdate")?.GetValue(handler);
if (update is not WUpdate wUpdate)
throw new InvalidCastException();
return wUpdate;
}
}
public TLUpdate? TLUpdate
{
get => handler.WUpdate.TLUpdate;
}
}
public static WUpdate AsWUpdate(this Update update)
{
return update as WUpdate
?? throw new InvalidCastException("Update is not assignable to `WTelegram.Types.Update`");
}
}
/// <summary>
/// Provides extension methods for <see cref="IHostApplicationBuilder"/> to configure Telegrator.
/// </summary>
public static class ServiceCollectionExtensions
{
public static IServiceCollection ConfigureWideTelegram(this IServiceCollection services, WTelegramBotClientOptions options)
{
services.AddSingleton(Options.Create(options));
return services;
}
/// <summary>
/// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers.
/// </summary>
public static IHostApplicationBuilder AddWideTelegrator(this IHostApplicationBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null, Action<ITelegramBotHostBuilder>? action = null)
{
AddTelegratorInternal(builder.Services, builder.Configuration, builder.Properties, ref handlers, options);
action?.Invoke(new TelegramBotHostBuilder(builder, handlers));
return builder;
}
/// <summary>
/// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers.
/// </summary>
public static IHostApplicationBuilder AddWideTelegrator(this IHostApplicationBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null)
{
AddTelegratorInternal(builder.Services, builder.Configuration, builder.Properties, ref handlers, options);
return builder;
}
/// <summary>
/// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers.
/// </summary>
internal static void AddTelegratorInternal(IServiceCollection services, IConfiguration configuration, IDictionary<object, object> properties, [NotNull] ref IHandlersCollection? handlers, TelegratorOptions? options = null)
{
if (services.Any(srvc => srvc.ServiceType == typeof(HostedUpdateReceiver)))
throw new InvalidOperationException("`HostedUpdateReceiver` found in services. WideHost extension is not compatible with long-polling receiving. Please, remove `AddTelegrator` invocation from your WebApp configuration.");
if (options == null)
{
options = configuration.GetSection(nameof(TelegratorOptions)).Get<TelegratorOptions>();
if (options == null)
throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegratorOptions' was registered. This configuration is runtime required!");
}
CancellationTokenSource globallCancell = new CancellationTokenSource();
options.GlobalCancellationToken = globallCancell.Token;
services.AddSingleton(Options.Create(options));
services.AddKeyedSingleton("cancell", globallCancell);
if (handlers != null)
{
if (handlers is IHandlersManager manager)
{
ServiceDescriptor descriptor = new ServiceDescriptor(typeof(IHandlersProvider), manager);
services.Replace(descriptor);
services.AddSingleton(manager);
}
}
handlers ??= new HostHandlersCollection(services, options);
services.AddSingleton(handlers);
properties.Add(HostBuilderExtensions.HandlersCollectionPropertyKey, handlers);
if (!services.Any(srvc => srvc.ImplementationType == typeof(IOptions<WTelegramBotClientOptions>)))
{
// For now, there's no way to configure this from IConfiguration, use `ConfigureWideTelegram` instead
throw new MissingMemberException("No options of type 'WTelegramBotClientOptions' was registered. This configuration is runtime required! Use `ConfigureWideTelegram` to register options.");
/*
services.AddSingleton(Options.Create(new WTelegramBotClientOptions(options.Token, options.BaseUrl, options.UseTestEnvironment)
{
RetryCount = options.RetryCount,
RetryThreshold = options.RetryThreshold
}));
*/
}
services.AddTelegramBotHostDefaults();
services.AddMTProtoUpdateReceiver();
}
public static IServiceCollection AddMTProtoUpdateReceiver(this IServiceCollection services)
{
services.AddHttpClient<WTelegramBotClient>("tgmtproto").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory);
services.AddSingleton<ITelegramBotClient>(sp => sp.GetRequiredService<WTelegramBotClient>());
services.AddHostedService<HostedWideBotUpdateReceiver>();
return services;
}
private static WTelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider)
=> new WTelegramBotClient(provider.GetRequiredService<IOptions<WTelegramBotClientOptions>>().Value, httpClient);
}
/// <summary>
/// Provides useful methods to adjust Telegram bot Host
/// </summary>
public static class TelegramBotHostExtensions
{
public static IHost UseWideTelegrator(this IHost botHost)
{
if (!botHost.Services.TryFindWTelegramBotClient())
throw new InvalidOperationException("No service for type 'Telegram.Bot.WTelegramBotClient' has been registered. Invoke `AddWideTelegrator`");
ITelegramBotInfo info = botHost.Services.GetRequiredService<ITelegramBotInfo>();
IHandlersCollection handlers = botHost.Services.GetRequiredService<IHandlersCollection>();
ILoggerFactory loggerFactory = botHost.Services.GetRequiredService<ILoggerFactory>();
ILogger logger = loggerFactory.CreateLogger("Telegrator.Hosting.Web.TelegratorHost");
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Telegrator WIDE Bot Host started (Generic Host)");
logger.LogInformation("Receiving mode : MTProto");
logger.LogInformation("Telegram Bot : {firstname}, @{usrname}, id:{id},", info.User.FirstName ?? "[NULL]", info.User.Username ?? "[NULL]", info.User.Id);
logger.LogHandlers(handlers);
}
botHost.AddLoggingAdapter();
botHost.SetBotCommands();
return botHost;
}
private static bool TryFindWTelegramBotClient(this IServiceProvider services)
{
return services.GetServices<IHostedService>().Any(s => s is HostedWideBotUpdateReceiver);
}
}
@@ -0,0 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Telegrator;
public class WideReceiverOptions
{
}