From aba9cf40375a6fb5d2b63bbc78678051cb7a2453 Mon Sep 17 00:00:00 2001 From: gutii Date: Mon, 27 Apr 2026 09:56:44 +0400 Subject: [PATCH] * 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 --- Telegrator.slnx | 1 + .../GlobalSuppressions.cs | 1 + ...plicitHandlerBuilderExtensionsGenerator.cs | 4 +- .../RoslynExtensions/CollectionsExtensions.cs | 115 ++++--- docs/Telegrator.Hosting.Web.xml | 155 +-------- docs/Telegrator.Hosting.xml | 101 ++---- docs/Telegrator.xml | 35 ++- .../DeveloperHelperAnalyzer.cs | 16 +- ...enerator.cs => KeyboardMarkupGenerator.cs} | 32 +- .../RoslynExtensions/Exceptions.cs | 10 +- .../GlobalSuppressions.cs | 2 + .../Hosting.Web/TelegramBotWebHost.cs | 159 ---------- .../Hosting.Web/TelegramBotWebHostBuilder.cs | 75 ----- src/Telegrator.Hosting.Web/Program.cs | 42 +-- src/Telegrator.Hosting.Web/TypesExtensions.cs | 296 +++++++++--------- .../GlobalSuppressions.cs | 8 + .../Mediation/HostedWideBotUpdateReceiver.cs | 35 +++ .../Mediation/WideUpdateReceiver.cs | 50 +++ .../Telegrator.Hosting.WideBot.csproj | 18 ++ .../TelegratorWideClient.cs | 76 +++++ .../TypesExtensions.cs | 181 +++++++++++ .../WideReceiverOptions.cs | 9 + src/Telegrator.Hosting/GlobalSuppressions.cs | 1 + .../Hosting/ITelegramBotHostBuilder.cs | 5 +- .../Hosting/TelegramBotHost.cs | 114 ------- .../Hosting/TelegramBotHostBuilder.cs | 19 +- .../HostUpdateRouter.cs | 3 +- .../HostedUpdateReceiver.cs | 6 +- .../Providers/HostHandlersCollection.cs | 41 ++- src/Telegrator.Hosting/TypesExtensions.cs | 63 ++-- .../Core/Handlers/UpdateHandlerBase.cs | 6 +- src/Telegrator/Mediation/UpdateRouter.cs | 47 ++- src/Telegrator/Result.cs | 22 +- src/Telegrator/SimpleTypesExtensions.cs | 19 +- src/Telegrator/TelegratorClient.cs | 13 +- src/Telegrator/TypesExtensions.cs | 24 +- 36 files changed, 814 insertions(+), 990 deletions(-) rename src/Telegrator.Analyzers/{GeneratedKeyboardMarkupGenerator.cs => KeyboardMarkupGenerator.cs} (95%) delete mode 100644 src/Telegrator.Hosting.Web/Hosting.Web/TelegramBotWebHost.cs delete mode 100644 src/Telegrator.Hosting.Web/Hosting.Web/TelegramBotWebHostBuilder.cs create mode 100644 src/Telegrator.Hosting.WideBot/GlobalSuppressions.cs create mode 100644 src/Telegrator.Hosting.WideBot/Mediation/HostedWideBotUpdateReceiver.cs create mode 100644 src/Telegrator.Hosting.WideBot/Mediation/WideUpdateReceiver.cs create mode 100644 src/Telegrator.Hosting.WideBot/Telegrator.Hosting.WideBot.csproj create mode 100644 src/Telegrator.Hosting.WideBot/TelegratorWideClient.cs create mode 100644 src/Telegrator.Hosting.WideBot/TypesExtensions.cs create mode 100644 src/Telegrator.Hosting.WideBot/WideReceiverOptions.cs delete mode 100644 src/Telegrator.Hosting/Hosting/TelegramBotHost.cs rename src/Telegrator.Hosting/{Polling => Mediation}/HostUpdateRouter.cs (97%) rename src/Telegrator.Hosting/{Polling => Mediation}/HostedUpdateReceiver.cs (90%) diff --git a/Telegrator.slnx b/Telegrator.slnx index 4e2b0ed..259b22b 100644 --- a/Telegrator.slnx +++ b/Telegrator.slnx @@ -4,6 +4,7 @@ + diff --git a/dev/Telegrator.RoslynGenerators/GlobalSuppressions.cs b/dev/Telegrator.RoslynGenerators/GlobalSuppressions.cs index 361f387..51b6e82 100644 --- a/dev/Telegrator.RoslynGenerators/GlobalSuppressions.cs +++ b/dev/Telegrator.RoslynGenerators/GlobalSuppressions.cs @@ -6,3 +6,4 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Style", "IDE0090")] +[assembly: SuppressMessage("Roslynator", "RCS1037")] diff --git a/dev/Telegrator.RoslynGenerators/ImplicitHandlerBuilderExtensionsGenerator.cs b/dev/Telegrator.RoslynGenerators/ImplicitHandlerBuilderExtensionsGenerator.cs index 88ff995..9c00486 100644 --- a/dev/Telegrator.RoslynGenerators/ImplicitHandlerBuilderExtensionsGenerator.cs +++ b/dev/Telegrator.RoslynGenerators/ImplicitHandlerBuilderExtensionsGenerator.cs @@ -112,7 +112,7 @@ public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator { try { - PrimaryConstructorBaseTypeSyntax primaryConstructor = (PrimaryConstructorBaseTypeSyntax)classDeclaration.BaseList.Types.ElementAt(0); + PrimaryConstructorBaseTypeSyntax primaryConstructor = (PrimaryConstructorBaseTypeSyntax)classDeclaration.BaseList.Types[0]; MethodDeclarationSyntax genExtension = GeneratedExtensionsMethod(classDeclaration, classDeclaration.ParameterList, primaryConstructor.ArgumentList, targeter); extensions.Add(genExtension); } @@ -239,7 +239,7 @@ public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator if (targeters.TryGetValue(classDeclaration.Identifier.ValueText, out MethodDeclarationSyntax targeter)) return targeter; - if (classDeclaration.BaseList != null && targeters.TryGetValue(classDeclaration.BaseList.Types.ElementAt(0).Type.ToString(), out targeter)) + if (classDeclaration.BaseList != null && targeters.TryGetValue(classDeclaration.BaseList.Types[0].Type.ToString(), out targeter)) return targeter; return null; diff --git a/dev/Telegrator.RoslynGenerators/RoslynExtensions/CollectionsExtensions.cs b/dev/Telegrator.RoslynGenerators/RoslynExtensions/CollectionsExtensions.cs index bac7255..9c63834 100644 --- a/dev/Telegrator.RoslynGenerators/RoslynExtensions/CollectionsExtensions.cs +++ b/dev/Telegrator.RoslynGenerators/RoslynExtensions/CollectionsExtensions.cs @@ -1,64 +1,63 @@ -namespace Telegrator.RoslynGenerators.RoslynExtensions +namespace Telegrator.RoslynGenerators.RoslynExtensions; + +public static class CollectionsExtensions { - public static class CollectionsExtensions + public static IEnumerable Combine(params IEnumerable[] collections) + => collections.SelectMany(x => x); + + public static IEnumerable IntersectBy(this IEnumerable first, IEnumerable second, Func selector) { - public static IEnumerable Combine(params IEnumerable[] collections) - => collections.SelectMany(x => x); - - public static IEnumerable IntersectBy(this IEnumerable first, IEnumerable second, Func selector) + foreach (TSource item in first) { - foreach (TSource item in first) - { - TValue value = selector(item); - if (second.Contains(value)) - yield return item; - } + TValue value = selector(item); + if (second.Contains(value)) + yield return item; } - - public static IList UnionAdd(this IList source, IEnumerable toUnion, IEqualityComparer comparer) - { - foreach (TValue toUnionValue in toUnion) - { - if (!source.Contains(toUnionValue, comparer)) - source.Add(toUnionValue); - } - - return source; - } - - public static void UnionAdd(this ICollection collection, IEnumerable target) - { - foreach (TSource item in target) - { - if (!collection.Contains(item)) - collection.Add(item); - } - } - - public static void UnionAdd(this SortedList collection, IEnumerable target) - { - foreach (TSource item in target) - { - if (!collection.Values.Contains(item)) - collection.Add(item, item); - } - } - - public static int IndexOf(this IEnumerable source, Func predicate) - { - int index = 0; - foreach (T item in source) - { - if (predicate.Invoke(item)) - return index; - - index++; - } - - return -1; - } - - public static IEnumerable Repeat(this T item, int times) - => Enumerable.Range(0, times).Select(_ => item); } + + public static IList UnionAdd(this IList source, IEnumerable toUnion, IEqualityComparer comparer) + { + foreach (TValue toUnionValue in toUnion) + { + if (!source.Contains(toUnionValue, comparer)) + source.Add(toUnionValue); + } + + return source; + } + + public static void UnionAdd(this ICollection collection, IEnumerable target) + { + foreach (TSource item in target) + { + if (!collection.Contains(item)) + collection.Add(item); + } + } + + public static void UnionAdd(this SortedList collection, IEnumerable target) + { + foreach (TSource item in target) + { + if (!collection.Values.Contains(item)) + collection.Add(item, item); + } + } + + public static int IndexOf(this IEnumerable source, Func predicate) + { + int index = 0; + foreach (T item in source) + { + if (predicate.Invoke(item)) + return index; + + index++; + } + + return -1; + } + + public static IEnumerable Repeat(this T item, int times) + => Enumerable.Range(0, times).Select(_ => item); } diff --git a/docs/Telegrator.Hosting.Web.xml b/docs/Telegrator.Hosting.Web.xml index a4e606a..04ff5db 100644 --- a/docs/Telegrator.Hosting.Web.xml +++ b/docs/Telegrator.Hosting.Web.xml @@ -4,131 +4,6 @@ Telegrator.Hosting.Web - - - Represents a web hosted telegram bot - - - - - - - - - - - - - - Allows consumers to be notified of application lifetime events. - - - - - This application's logger - - - - - - - - Initializes a new instance of the class. - - The proxied instance of host builder. - - - - Creates new with default services and webhook update receiving scheme - - - - - - Creates new SLIM with default services and webhook update receiving scheme - - - - - - Creates new EMPTY WITHOUT any services or update receiving schemes - - - - - - - - - - - - - - - - - - - - - - - - Disposes the host. - - - - - Disposes the host. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initializes a new instance of the class. - - - - - - Initializes a new instance of the class. - - - - - - - Builds the host. - - - - - - Configuration options for Telegram bot behavior and execution settings. @@ -200,20 +75,12 @@ Provides method to configure Telegram Bot WebHost - - - The key used to store the in the builder properties. - - - - - - + Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers. - + Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers. @@ -223,7 +90,12 @@ Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers. - + + + Provides useful methods to adjust Telegram bot Host + + + Searchs for hosted service inside hosts services @@ -231,13 +103,13 @@ - + Replaces the initialization logic from TelegramBotWebHost constructor. Initializes the bot and logs handlers on application startup. - + Allows to remap receiving webhook endpoint and map new route to webhost. @@ -246,17 +118,12 @@ - + Registers service with to receive updates using webhook - - - Gets the from the builder properties. - - diff --git a/docs/Telegrator.Hosting.xml b/docs/Telegrator.Hosting.xml index ec77187..05914c8 100644 --- a/docs/Telegrator.Hosting.xml +++ b/docs/Telegrator.Hosting.xml @@ -40,63 +40,6 @@ Represents a hosted telegram bots and services builder that helps manage configuration, logging, lifetime, and more. - - - Represents a hosted telegram bot - - - - - - - - - - - This application's logger - - - - - Initializes a new instance of the class. - - The proxied instance of host builder. - - - - Creates new with default configuration, services and long-polling update receiving scheme - - - - - - Creates new with default services and long-polling update receiving scheme - - - - - - Creates new EMPTY WITHOUT any services or update receiving schemes - - - - - - Creates new EMPTY WITHOUT any services or update receiving schemes - - - - - - - - - - - - Disposes the host. - - @@ -121,25 +64,19 @@ - + Initializes a new instance of the class. - + Initializes a new instance of the class. - - - Builds the host. - - - @@ -158,7 +95,7 @@ - + Service for receiving updates for Hosted telegram bots @@ -167,7 +104,7 @@ - + Service for receiving updates for Hosted telegram bots @@ -176,24 +113,24 @@ - + - + - + of this router - + - + - + Default exception handler of this router @@ -239,12 +176,7 @@ The key used to store the in the builder properties. - - - Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. - - - + Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. @@ -259,6 +191,11 @@ Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. + + + Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. + + Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. @@ -280,14 +217,14 @@ - Registers default services + Registers default services - Registers service with to receive updates using long polling + Registers service with to receive updates using long polling @@ -307,7 +244,7 @@ - Replaces the initialization logic from TelegramBotWebHost constructor. + Replaces the initialization logic from TelegramBotWebHost constructor. Initializes the bot and logs handlers on application startup. diff --git a/docs/Telegrator.xml b/docs/Telegrator.xml index 71bbd19..d43eb31 100644 --- a/docs/Telegrator.xml +++ b/docs/Telegrator.xml @@ -6027,6 +6027,9 @@ Manages the distribution of updates between regular handlers and awaiting handlers. + + + @@ -6036,9 +6039,6 @@ - - - @@ -6377,14 +6377,9 @@ Represents handler results, allowing to communicate with router and control aspect execution - + - Is result positive - - - - - Should router search for next matching handler + Tell router to stop describing @@ -6449,6 +6444,14 @@ + + + Remove all values and returns collection without nullable type. + + + + + Enumerates objects in a and executes an on each one @@ -6558,7 +6561,7 @@ - + Checks if is an implementation of class or its descendants @@ -6802,7 +6805,7 @@ Initializes the update router and begins polling for updates. Optional receiver options for configuring update polling. - The cancellation token to stop receiving updates. + The cancellation token to stop receiving updates. @@ -7113,6 +7116,14 @@ Provides convenient methods for creating implicit handlers. + + + Collects all handlers from current app domain. + Scans for handlers exported by analyzer into class `Telegrator.Analyzers.AnalyzerExport` in each assembly and registers them to the collection. + + + + Collects all public handlers from the current app domain. diff --git a/src/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs b/src/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs index 4e84bcc..0e457e3 100644 --- a/src/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs +++ b/src/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs @@ -10,6 +10,8 @@ namespace Telegrator.Analyzers; [Generator(LanguageNames.CSharp)] public class DeveloperHelperAnalyzer : IIncrementalGenerator { + internal record class HandlerDeclarationModel(string ClassName, string NamespaceName, string? AttributeName, string? BaseClassName, Location Location); + private static readonly DiagnosticDescriptor MissingBaseClassWarning = new( id: "TLG101", title: "Missing handlers base class", @@ -131,9 +133,8 @@ public class DeveloperHelperAnalyzer : IIncrementalGenerator private static FieldDeclarationSyntax GenerateTypeField(HandlerDeclarationModel handler) { - string fullTypeName = handler.Namespace == "Global" - ? handler.ClassName - : $"{handler.Namespace}.{handler.ClassName}"; + string fullTypeName = handler.NamespaceName == "Global" + ? handler.ClassName : $"{handler.NamespaceName}.{handler.ClassName}"; TypeOfExpressionSyntax typeofExpression = SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(fullTypeName)); VariableDeclaratorSyntax variableDeclarator = SyntaxFactory.VariableDeclarator(SyntaxFactory.Identifier($"{handler.ClassName}Type")) @@ -149,15 +150,6 @@ public class DeveloperHelperAnalyzer : IIncrementalGenerator } } -internal class HandlerDeclarationModel(string className, string namespaceName, string? attributeName, string? baseClassName, Location location) -{ - public readonly string ClassName = className; - public readonly string Namespace = namespaceName; - public readonly string? AttributeName = attributeName; - public readonly string? BaseClassName = baseClassName; - public readonly Location Location = location; -} - internal static class DeveloperHelperAnalyzerExtensions { private static readonly string[] HandlersNames = diff --git a/src/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs b/src/Telegrator.Analyzers/KeyboardMarkupGenerator.cs similarity index 95% rename from src/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs rename to src/Telegrator.Analyzers/KeyboardMarkupGenerator.cs index 655fdb2..70d4345 100644 --- a/src/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs +++ b/src/Telegrator.Analyzers/KeyboardMarkupGenerator.cs @@ -8,8 +8,12 @@ using System.Text; namespace Telegrator.Analyzers; [Generator(LanguageNames.CSharp)] -public class GeneratedKeyboardMarkupGenerator : IIncrementalGenerator +public class KeyboardMarkupGenerator : IIncrementalGenerator { + // Records + private record class GeneratedMarkupMethodModel(MethodDeclarationSyntax OriginalMethod, FieldDeclarationSyntax GeneratedField, MethodDeclarationSyntax GeneratedMethod); + private record class GeneratedMarkupPropertyModel(PropertyDeclarationSyntax OriginalProperty, PropertyDeclarationSyntax GeneratedProperty); + // Return types private const string InlineReturnType = "InlineKeyboardMarkup"; private const string ReplyReturnType = "ReplyKeyboardMarkup"; @@ -459,30 +463,4 @@ public class GeneratedKeyboardMarkupGenerator : IIncrementalGenerator SyntaxFactory.IdentifierName(className), SyntaxFactory.IdentifierName(methodName)); } - - private class GeneratedMarkupMethodModel - { - public MethodDeclarationSyntax OriginalMethod { get; } - public FieldDeclarationSyntax GeneratedField { get; } - public MethodDeclarationSyntax GeneratedMethod { get; } - - public GeneratedMarkupMethodModel(MethodDeclarationSyntax originalMethod, FieldDeclarationSyntax generatedField, MethodDeclarationSyntax generatedMethod) - { - OriginalMethod = originalMethod; - GeneratedField = generatedField; - GeneratedMethod = generatedMethod; - } - } - - private class GeneratedMarkupPropertyModel - { - public PropertyDeclarationSyntax OriginalProperty { get; } - public PropertyDeclarationSyntax GeneratedProperty { get; } - - public GeneratedMarkupPropertyModel(PropertyDeclarationSyntax originalProperty, PropertyDeclarationSyntax generatedProperty) - { - OriginalProperty = originalProperty; - GeneratedProperty = generatedProperty; - } - } } \ No newline at end of file diff --git a/src/Telegrator.Analyzers/RoslynExtensions/Exceptions.cs b/src/Telegrator.Analyzers/RoslynExtensions/Exceptions.cs index 7124a8b..a958cf0 100644 --- a/src/Telegrator.Analyzers/RoslynExtensions/Exceptions.cs +++ b/src/Telegrator.Analyzers/RoslynExtensions/Exceptions.cs @@ -1,7 +1,7 @@ namespace Telegrator.Analyzers.RoslynExtensions; -public class TargteterNotFoundException() : Exception() { } - -public class BaseClassTypeNotFoundException() : Exception() { } - -public class AncestorNotFoundException : Exception { } +#pragma warning disable RCS1194 // Implement exception constructors +public class TargteterNotFoundException() : Exception(); +public class BaseClassTypeNotFoundException() : Exception(); +public class AncestorNotFoundException() : Exception(); +#pragma warning restore RCS1194 // Implement exception constructors diff --git a/src/Telegrator.Hosting.Web/GlobalSuppressions.cs b/src/Telegrator.Hosting.Web/GlobalSuppressions.cs index 691de9c..a017ac2 100644 --- a/src/Telegrator.Hosting.Web/GlobalSuppressions.cs +++ b/src/Telegrator.Hosting.Web/GlobalSuppressions.cs @@ -10,3 +10,5 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Usage", "CA2254")] [assembly: SuppressMessage("Maintainability", "CA1510")] [assembly: SuppressMessage("Style", "IDE0270")] +[assembly: SuppressMessage("Roslynator", "RCS1037")] +[assembly: SuppressMessage("Roslynator", "RCS1224")] diff --git a/src/Telegrator.Hosting.Web/Hosting.Web/TelegramBotWebHost.cs b/src/Telegrator.Hosting.Web/Hosting.Web/TelegramBotWebHost.cs deleted file mode 100644 index 38b96c4..0000000 --- a/src/Telegrator.Hosting.Web/Hosting.Web/TelegramBotWebHost.cs +++ /dev/null @@ -1,159 +0,0 @@ -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 Telegrator.Core; - -namespace Telegrator.Hosting.Web; - -/// -/// Represents a web hosted telegram bot -/// -public class TelegramBotWebHost : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable -{ - private readonly WebApplication _innerApp; - private readonly IUpdateRouter _updateRouter; - private readonly ILogger _logger; - - private bool _disposed; - - /// - public IServiceProvider Services => _innerApp.Services; - - /// - public IUpdateRouter UpdateRouter => _updateRouter; - - /// - public ICollection DataSources => ((IEndpointRouteBuilder)_innerApp).DataSources; - - /// - /// Allows consumers to be notified of application lifetime events. - /// - public IHostApplicationLifetime Lifetime => _innerApp.Lifetime; - - /// - /// This application's logger - /// - public ILogger Logger => _logger; - - /// - public IDictionary Properties => ((IApplicationBuilder)_innerApp).Properties; - - // Private interface fields - IServiceProvider IEndpointRouteBuilder.ServiceProvider => Services; - IServiceProvider IApplicationBuilder.ApplicationServices { get => Services; set => throw new NotImplementedException(); } - IFeatureCollection IApplicationBuilder.ServerFeatures => ((IApplicationBuilder)_innerApp).ServerFeatures; - - /// - /// Initializes a new instance of the class. - /// - /// The proxied instance of host builder. - public TelegramBotWebHost(WebApplicationBuilder webApplicationBuilder) - { - // Building proxy application - _innerApp = webApplicationBuilder.Build(); - - // Reruesting services for this host - _updateRouter = Services.GetRequiredService(); - _logger = Services.GetRequiredService>(); - } - - /// - /// Creates new with default services and webhook update receiving scheme - /// - /// - public static TelegramBotWebHostBuilder CreateBuilder(WebApplicationOptions? settings) - { - ArgumentNullException.ThrowIfNull(settings, nameof(settings)); - WebApplicationBuilder innerApp = WebApplication.CreateBuilder(settings); - TelegramBotWebHostBuilder builder = new TelegramBotWebHostBuilder(innerApp); - builder.AddTelegratorWeb(); - return builder; - } - - /// - /// Creates new SLIM with default services and webhook update receiving scheme - /// - /// - public static TelegramBotWebHostBuilder CreateSlimBuilder(WebApplicationOptions? settings) - { - ArgumentNullException.ThrowIfNull(settings, nameof(settings)); - WebApplicationBuilder innerApp = WebApplication.CreateSlimBuilder(settings); - TelegramBotWebHostBuilder builder = new TelegramBotWebHostBuilder(innerApp); - builder.AddTelegratorWeb(); - return builder; - } - - /// - /// Creates new EMPTY WITHOUT any services or update receiving schemes - /// - /// - public static TelegramBotWebHostBuilder CreateEmptyBuilder(WebApplicationOptions? settings) - { - ArgumentNullException.ThrowIfNull(settings, nameof(settings)); - WebApplicationBuilder innerApp = WebApplication.CreateEmptyBuilder(settings); - TelegramBotWebHostBuilder builder = new TelegramBotWebHostBuilder(innerApp); - builder.AddTelegratorWeb(); - return builder; - } - - /// - public async Task StartAsync(CancellationToken cancellationToken = default) - { - await _innerApp.StartAsync(cancellationToken); - } - - /// - public async Task StopAsync(CancellationToken cancellationToken = default) - { - await _innerApp.StopAsync(cancellationToken); - } - - /// - public IApplicationBuilder CreateApplicationBuilder() - => ((IEndpointRouteBuilder)_innerApp).CreateApplicationBuilder(); - - /// - public IApplicationBuilder Use(Func middleware) - => _innerApp.Use(middleware); - - /// - public IApplicationBuilder New() - => ((IApplicationBuilder)_innerApp).New(); - - /// - public RequestDelegate Build() - => ((IApplicationBuilder)_innerApp).Build(); - - /// - /// Disposes the host. - /// - public async ValueTask DisposeAsync() - { - if (_disposed) - return; - - await _innerApp.DisposeAsync(); - - GC.SuppressFinalize(this); - _disposed = true; - } - - /// - /// Disposes the host. - /// - public void Dispose() - { - if (_disposed) - return; - - ValueTask disposeTask = _innerApp.DisposeAsync(); - disposeTask.AsTask().Wait(); - - GC.SuppressFinalize(this); - _disposed = true; - } -} diff --git a/src/Telegrator.Hosting.Web/Hosting.Web/TelegramBotWebHostBuilder.cs b/src/Telegrator.Hosting.Web/Hosting.Web/TelegramBotWebHostBuilder.cs deleted file mode 100644 index fb813ba..0000000 --- a/src/Telegrator.Hosting.Web/Hosting.Web/TelegramBotWebHostBuilder.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.Metrics; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Telegrator.Core; - -#pragma warning disable IDE0001 -namespace Telegrator.Hosting.Web; - -/// -public class TelegramBotWebHostBuilder : ITelegramBotHostBuilder -{ - private readonly WebApplicationBuilder _innerBuilder; - internal IHandlersCollection _handlers = null!; - - /// - public IHandlersCollection Handlers => _handlers; - - /// - public IConfigurationManager Configuration => _innerBuilder.Configuration; - - /// - public ILoggingBuilder Logging => _innerBuilder.Logging; - - /// - public IServiceCollection Services => _innerBuilder.Services; - - /// - public IHostEnvironment Environment => _innerBuilder.Environment; - - /// - public IDictionary Properties => ((IHostApplicationBuilder)_innerBuilder).Properties; - - /// - public IMetricsBuilder Metrics => _innerBuilder.Metrics; - - /// - /// Initializes a new instance of the class. - /// - /// - public TelegramBotWebHostBuilder(WebApplicationBuilder webApplicationBuilder) - { - _innerBuilder = webApplicationBuilder ?? throw new ArgumentNullException(nameof(webApplicationBuilder)); - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public TelegramBotWebHostBuilder(WebApplicationBuilder webApplicationBuilder, IHandlersCollection handlers) - { - _innerBuilder = webApplicationBuilder ?? throw new ArgumentNullException(nameof(webApplicationBuilder)); - _handlers = handlers ?? throw new ArgumentNullException(nameof(handlers)); - } - - /// - /// Builds the host. - /// - /// - public TelegramBotWebHost Build() - { - TelegramBotWebHost host = new TelegramBotWebHost(_innerBuilder); - host.UseTelegratorWeb(); - return host; - } - - /// - public void ConfigureContainer(IServiceProviderFactory factory, Action? configure = null) where TContainerBuilder : notnull - { - ((IHostApplicationBuilder)_innerBuilder).ConfigureContainer(factory, configure); - } -} diff --git a/src/Telegrator.Hosting.Web/Program.cs b/src/Telegrator.Hosting.Web/Program.cs index 67c3c8c..a2cfcec 100644 --- a/src/Telegrator.Hosting.Web/Program.cs +++ b/src/Telegrator.Hosting.Web/Program.cs @@ -6,25 +6,8 @@ using Telegrator.Hosting.Web; namespace Telegrator; -internal class Program +internal static class Program { - public static void TelegramBotWebHostBuilder_Example(string[] args) - { - TelegramBotWebHostBuilder builder = TelegramBotWebHost.CreateBuilder(new WebApplicationOptions() - { - Args = args, - ApplicationName = "TelegramBotWebHost example", - }); - - builder.Handlers - .CollectHandlersAssemblyWide(); - - builder.Build() - .AddLoggingAdapter() - .SetBotCommands() - .Run(); - } - public static void WebApplicationBuilder_Example(string[] args) { WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions() @@ -38,28 +21,7 @@ internal class Program builder.Build() .UseTelegratorWeb(dontMap: true) - .RemapWebhook("https://awesome-butt-sex.cloudpub.ru/") - .AddLoggingAdapter() - .SetBotCommands() - .Run(); - } - - public static void TelegramBotHostBuilder_Example(string[] args) - { - ConfigurationManager configuration = new ConfigurationManager(); - configuration.AddJsonFile("appsettings.json"); - - TelegramBotHostBuilder builder = TelegramBotHost.CreateBuilder(new HostApplicationBuilderSettings() - { - Args = args, - ApplicationName = "TelegramBotHost example", - Configuration = configuration - }); - - builder.Handlers - .CollectHandlersAssemblyWide(); - - builder.Build() + .RemapWebhook("https://amazing-butt-sex.cloudpub.ru/") .AddLoggingAdapter() .SetBotCommands() .Run(); diff --git a/src/Telegrator.Hosting.Web/TypesExtensions.cs b/src/Telegrator.Hosting.Web/TypesExtensions.cs index b4676a3..4a038d3 100644 --- a/src/Telegrator.Hosting.Web/TypesExtensions.cs +++ b/src/Telegrator.Hosting.Web/TypesExtensions.cs @@ -8,169 +8,175 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Diagnostics.CodeAnalysis; using Telegram.Bot; +using Telegram.Bot.Polling; using Telegrator.Core; using Telegrator.Hosting; using Telegrator.Hosting.Web; using Telegrator.Mediation; using Telegrator.Providers; -namespace Telegrator +namespace Telegrator; + +/// +/// Contains extensions for +/// Provides method to configure Telegram Bot WebHost +/// +public static class ServicesCollectionExtensions { - /// - /// Contains extensions for - /// Provides method to configure Telegram Bot WebHost - /// - public static class ServicesCollectionExtensions + public static IServiceCollection ConfigureWebhooker(this IServiceCollection services, WebhookerOptions options) { - /// - /// The key used to store the in the builder properties. - /// - public const string HandlersCollectionPropertyKey = nameof(IHandlersCollection); + services.AddSingleton(Options.Create(options)); + return services; + } - /// - /// Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers. - /// - public static ITelegramBotHostBuilder AddTelegratorWeb(this ITelegramBotHostBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null, Action? action = null) + /// + /// Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers. + /// + public static IHostApplicationBuilder AddTelegratorWeb(this IHostApplicationBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null) + { + AddTelegratorWebInternal(builder.Services, builder.Configuration, builder.Properties, ref handlers, options); + return builder; + } + + /// + /// Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers. + /// + public static IHostApplicationBuilder AddTelegratorWeb(this IHostApplicationBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null, Action? action = null) + { + AddTelegratorWebInternal(builder.Services, builder.Configuration, builder.Properties, ref handlers, options); + action?.Invoke(new TelegramBotHostBuilder(builder, handlers)); + return builder; + } + + /// + /// Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers. + /// + internal static void AddTelegratorWebInternal(IServiceCollection services, IConfiguration configuration, IDictionary properties, [NotNull] ref IHandlersCollection? handlers, TelegratorOptions? options = null) + { + if (services.Any(srvc => srvc.ServiceType == typeof(HostedUpdateReceiver))) + throw new InvalidOperationException("`HostedUpdateReceiver` found in services. WebHost extension is not compatible with long-polling receiving. Please, remove `AddTelegrator` invocation from your WebApp configuration."); + + if (options is null) { - AddTelegratorWebInternal(builder.Services, builder.Configuration, builder.Properties, ref handlers, options); - if (builder is TelegramBotWebHostBuilder telegramBotHostBuilder) - telegramBotHostBuilder._handlers = handlers; - - action?.Invoke(builder); - return builder; + options = configuration.GetSection(nameof(TelegratorOptions)).Get(); + if (options is null) + throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegratorOptions' was registered. This configuration is runtime required!"); } - /// - /// Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers. - /// - public static IHostApplicationBuilder AddTelegratorWeb(this WebApplicationBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null, Action? action = null) + CancellationTokenSource globallCancell = new CancellationTokenSource(); + options.GlobalCancellationToken = globallCancell.Token; + services.AddSingleton(Options.Create(options)); + services.AddKeyedSingleton("cancell", globallCancell); + + if (handlers is not null && handlers is IHandlersManager manager) { - AddTelegratorWebInternal(builder.Services, builder.Configuration, ((IHostApplicationBuilder)builder).Properties, ref handlers, options); - action?.Invoke(new TelegramBotWebHostBuilder(builder, handlers)); - return builder; + ServiceDescriptor descriptor = new ServiceDescriptor(typeof(IHandlersProvider), manager); + services.Replace(descriptor); + services.AddSingleton(manager); } - /// - /// Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers. - /// - internal static void AddTelegratorWebInternal(IServiceCollection services, IConfiguration configuration, IDictionary properties, [NotNull] ref IHandlersCollection? handlers, TelegratorOptions? options = null) + handlers ??= new HostHandlersCollection(services, options); + services.AddSingleton(handlers); + properties.Add(HostBuilderExtensions.HandlersCollectionPropertyKey, handlers); + + if (!services.Any(srvc => srvc.ServiceType == typeof(IOptions))) { - if (options == null) + WebhookerOptions? webhookerOptions = configuration.GetSection(nameof(WebhookerOptions)).Get(); + if (webhookerOptions == null) + throw new MissingMemberException("Auto configuration disabled, yet no options of type 'WebhookerOptions' was registered. This configuration is runtime required!"); + + services.AddSingleton(Options.Create(webhookerOptions)); + } + + if (!services.Any(srvc => srvc.ImplementationType == typeof(IOptions))) + { + services.AddSingleton(Options.Create(new TelegramBotClientOptions(options.Token, options.BaseUrl, options.UseTestEnvironment) { - options = configuration.GetSection(nameof(TelegratorOptions)).Get(); - if (options == null) - throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegratorOptions' wasn't 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(HandlersCollectionPropertyKey, handlers); - - if (!services.Any(srvc => srvc.ImplementationType == typeof(IOptions))) - { - WebhookerOptions? webhookerOptions = configuration.GetSection(nameof(WebhookerOptions)).Get(); - if (webhookerOptions == null) - throw new MissingMemberException("Auto configuration disabled, yet no options of type 'WebhookerOptions' wasn't registered. This configuration is runtime required!"); - - services.AddSingleton(Options.Create(webhookerOptions)); - } - - if (!services.Any(srvc => srvc.ImplementationType == typeof(IOptions))) - { - services.AddSingleton(Options.Create(new TelegramBotClientOptions(options.Token, options.BaseUrl, options.UseTestEnvironment) - { - RetryCount = options.RetryCount, - RetryThreshold = options.RetryThreshold - })); - } - - services.AddTelegramBotHostDefaults(); - services.AddTelegramWebhook(); + RetryCount = options.RetryCount, + RetryThreshold = options.RetryThreshold + })); } - /// - /// Searchs for hosted service inside hosts services - /// - /// - /// - /// - public static bool TryFindWebhooker(this IServiceProvider services, [NotNullWhen(true)] out HostedUpdateWebhooker? webhooker) - { - webhooker = services.GetServices().FirstOrDefault(s => s is HostedUpdateWebhooker) as HostedUpdateWebhooker; - return webhooker != null; - } - - /// - /// Replaces the initialization logic from TelegramBotWebHost constructor. - /// Initializes the bot and logs handlers on application startup. - /// - public static T UseTelegratorWeb(this T app, bool dontMap = false) where T : IEndpointRouteBuilder, IHost - { - if (!app.ServiceProvider.TryFindWebhooker(out HostedUpdateWebhooker? webhooker)) - throw new InvalidOperationException("No service for type 'Telegrator.Mediation.HostedUpdateWebhooker' has been registered."); - - ITelegramBotInfo info = app.ServiceProvider.GetRequiredService(); - IHandlersCollection handlers = app.ServiceProvider.GetRequiredService(); - ILoggerFactory loggerFactory = app.ServiceProvider.GetRequiredService(); - ILogger logger = loggerFactory.CreateLogger("Telegrator.Hosting.Web.TelegratorHost"); - - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("Telegrator Bot ASP.NET WebHost started"); - logger.LogInformation("Telegram Bot : {firstname}, @{usrname}, id:{id},", info.User.FirstName ?? "[NULL]", info.User.Username ?? "[NULL]", info.User.Id); - logger.LogHandlers(handlers); - } - - if (!dontMap) - webhooker.MapWebhook(app); - - return app; - } - - /// - /// Allows to remap receiving webhook endpoint and map new route to webhost. - /// - /// - /// - /// - /// - public static T RemapWebhook(this T app, string webhookUri) where T : IEndpointRouteBuilder, IHost - { - if (!app.ServiceProvider.TryFindWebhooker(out HostedUpdateWebhooker? webhooker)) - throw new InvalidOperationException("No service for type 'Telegrator.Mediation.HostedUpdateWebhooker' has been registered."); - - webhooker.RemapWebhook(app, webhookUri, default).GetAwaiter().GetResult(); - return app; - } - - /// - /// Registers service with to receive updates using webhook - /// - /// - /// - public static IServiceCollection AddTelegramWebhook(this IServiceCollection services) - { - services.AddHttpClient("tgwebhook").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory); - services.AddHostedService(); - return services; - } - - private static ITelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider) - => new TelegramBotClient(provider.GetRequiredService>().Value, httpClient); + services.AddTelegramBotHostDefaults(); + services.AddTelegramWebhook(); } } + +/// +/// Provides useful methods to adjust Telegram bot Host +/// +public static class TelegramBotHostExtensions +{ + /// + /// Searchs for hosted service inside hosts services + /// + /// + /// + /// + public static bool TryFindWebhooker(this IServiceProvider services, [NotNullWhen(true)] out HostedUpdateWebhooker? webhooker) + { + webhooker = services.GetServices().FirstOrDefault(s => s is HostedUpdateWebhooker) as HostedUpdateWebhooker; + return webhooker != null; + } + + /// + /// Replaces the initialization logic from TelegramBotWebHost constructor. + /// Initializes the bot and logs handlers on application startup. + /// + public static T UseTelegratorWeb(this T botHost, bool dontMap = false) where T : IEndpointRouteBuilder, IHost + { + if (!botHost.ServiceProvider.TryFindWebhooker(out HostedUpdateWebhooker? webhooker)) + throw new InvalidOperationException("No service for type 'Telegrator.Mediation.HostedUpdateWebhooker' has been registered."); + + ITelegramBotInfo info = botHost.ServiceProvider.GetRequiredService(); + IHandlersCollection handlers = botHost.ServiceProvider.GetRequiredService(); + ILoggerFactory loggerFactory = botHost.ServiceProvider.GetRequiredService(); + ILogger logger = loggerFactory.CreateLogger("Telegrator.Hosting.Web.TelegratorHost"); + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Telegrator Bot Host started (ASP.NET WebHost)"); + logger.LogInformation("Receiving mode : WEB-HOOKING"); + logger.LogInformation("Telegram Bot : {firstname}, @{usrname}, id:{id},", info.User.FirstName ?? "[NULL]", info.User.Username ?? "[NULL]", info.User.Id); + logger.LogHandlers(handlers); + } + + if (!dontMap) + webhooker.MapWebhook(botHost); + + botHost.AddLoggingAdapter(); + botHost.SetBotCommands(); + return botHost; + } + + /// + /// Allows to remap receiving webhook endpoint and map new route to webhost. + /// + /// + /// + /// + /// + public static T RemapWebhook(this T app, string webhookUri) where T : IEndpointRouteBuilder, IHost + { + if (!app.ServiceProvider.TryFindWebhooker(out HostedUpdateWebhooker? webhooker)) + throw new InvalidOperationException("No service for type 'Telegrator.Mediation.HostedUpdateWebhooker' has been registered."); + + webhooker.RemapWebhook(app, webhookUri, default).GetAwaiter().GetResult(); + return app; + } + + /// + /// Registers service with to receive updates using webhook + /// + /// + /// + public static IServiceCollection AddTelegramWebhook(this IServiceCollection services) + { + services.AddHttpClient("tgwebhook").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory); + services.AddHostedService(); + return services; + } + + private static ITelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider) + => new TelegramBotClient(provider.GetRequiredService>().Value, httpClient); +} diff --git a/src/Telegrator.Hosting.WideBot/GlobalSuppressions.cs b/src/Telegrator.Hosting.WideBot/GlobalSuppressions.cs new file mode 100644 index 0000000..3562098 --- /dev/null +++ b/src/Telegrator.Hosting.WideBot/GlobalSuppressions.cs @@ -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")] diff --git a/src/Telegrator.Hosting.WideBot/Mediation/HostedWideBotUpdateReceiver.cs b/src/Telegrator.Hosting.WideBot/Mediation/HostedWideBotUpdateReceiver.cs new file mode 100644 index 0000000..b49c9ba --- /dev/null +++ b/src/Telegrator.Hosting.WideBot/Mediation/HostedWideBotUpdateReceiver.cs @@ -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 logger, ITelegramBotClient botClient, IUpdateRouter updateRouter, IOptions? 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); + } +} diff --git a/src/Telegrator.Hosting.WideBot/Mediation/WideUpdateReceiver.cs b/src/Telegrator.Hosting.WideBot/Mediation/WideUpdateReceiver.cs new file mode 100644 index 0000000..5aafb75 --- /dev/null +++ b/src/Telegrator.Hosting.WideBot/Mediation/WideUpdateReceiver.cs @@ -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 tcs = new TaskCompletionSource(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); + } + } +} \ No newline at end of file diff --git a/src/Telegrator.Hosting.WideBot/Telegrator.Hosting.WideBot.csproj b/src/Telegrator.Hosting.WideBot/Telegrator.Hosting.WideBot.csproj new file mode 100644 index 0000000..f5a87c3 --- /dev/null +++ b/src/Telegrator.Hosting.WideBot/Telegrator.Hosting.WideBot.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + Telegrator + latest + + + + + + + + + + + diff --git a/src/Telegrator.Hosting.WideBot/TelegratorWideClient.cs b/src/Telegrator.Hosting.WideBot/TelegratorWideClient.cs new file mode 100644 index 0000000..4518d30 --- /dev/null +++ b/src/Telegrator.Hosting.WideBot/TelegratorWideClient.cs @@ -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)"); + } + } +} +*/ \ No newline at end of file diff --git a/src/Telegrator.Hosting.WideBot/TypesExtensions.cs b/src/Telegrator.Hosting.WideBot/TypesExtensions.cs new file mode 100644 index 0000000..0dd9759 --- /dev/null +++ b/src/Telegrator.Hosting.WideBot/TypesExtensions.cs @@ -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(AbstractUpdateHandler handler) where TUpdate : class + { + public WUpdate WUpdate + { + get + { + object? update = typeof(AbstractUpdateHandler).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`"); + } +} + +/// +/// Provides extension methods for to configure Telegrator. +/// +public static class ServiceCollectionExtensions +{ + public static IServiceCollection ConfigureWideTelegram(this IServiceCollection services, WTelegramBotClientOptions options) + { + services.AddSingleton(Options.Create(options)); + return services; + } + + /// + /// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. + /// + public static IHostApplicationBuilder AddWideTelegrator(this IHostApplicationBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null, Action? action = null) + { + AddTelegratorInternal(builder.Services, builder.Configuration, builder.Properties, ref handlers, options); + action?.Invoke(new TelegramBotHostBuilder(builder, handlers)); + return builder; + } + + /// + /// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. + /// + 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; + } + + /// + /// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. + /// + internal static void AddTelegratorInternal(IServiceCollection services, IConfiguration configuration, IDictionary 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(); + 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))) + { + // 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("tgmtproto").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddHostedService(); + return services; + } + + private static WTelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider) + => new WTelegramBotClient(provider.GetRequiredService>().Value, httpClient); +} + +/// +/// Provides useful methods to adjust Telegram bot Host +/// +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(); + IHandlersCollection handlers = botHost.Services.GetRequiredService(); + ILoggerFactory loggerFactory = botHost.Services.GetRequiredService(); + 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().Any(s => s is HostedWideBotUpdateReceiver); + } +} diff --git a/src/Telegrator.Hosting.WideBot/WideReceiverOptions.cs b/src/Telegrator.Hosting.WideBot/WideReceiverOptions.cs new file mode 100644 index 0000000..224b112 --- /dev/null +++ b/src/Telegrator.Hosting.WideBot/WideReceiverOptions.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Telegrator; + +public class WideReceiverOptions +{ +} diff --git a/src/Telegrator.Hosting/GlobalSuppressions.cs b/src/Telegrator.Hosting/GlobalSuppressions.cs index 691de9c..052bb70 100644 --- a/src/Telegrator.Hosting/GlobalSuppressions.cs +++ b/src/Telegrator.Hosting/GlobalSuppressions.cs @@ -10,3 +10,4 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Usage", "CA2254")] [assembly: SuppressMessage("Maintainability", "CA1510")] [assembly: SuppressMessage("Style", "IDE0270")] +[assembly: SuppressMessage("Roslynator", "RCS1037")] diff --git a/src/Telegrator.Hosting/Hosting/ITelegramBotHostBuilder.cs b/src/Telegrator.Hosting/Hosting/ITelegramBotHostBuilder.cs index 6cccc4d..efedb98 100644 --- a/src/Telegrator.Hosting/Hosting/ITelegramBotHostBuilder.cs +++ b/src/Telegrator.Hosting/Hosting/ITelegramBotHostBuilder.cs @@ -6,7 +6,4 @@ namespace Telegrator.Hosting; /// /// Represents a hosted telegram bots and services builder that helps manage configuration, logging, lifetime, and more. /// -public interface ITelegramBotHostBuilder : IHostApplicationBuilder, ICollectingProvider -{ - -} +public interface ITelegramBotHostBuilder : IHostApplicationBuilder, ICollectingProvider; diff --git a/src/Telegrator.Hosting/Hosting/TelegramBotHost.cs b/src/Telegrator.Hosting/Hosting/TelegramBotHost.cs deleted file mode 100644 index 65d9563..0000000 --- a/src/Telegrator.Hosting/Hosting/TelegramBotHost.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Telegrator.Core; - -namespace Telegrator.Hosting; - -/// -/// Represents a hosted telegram bot -/// -public class TelegramBotHost : IHost, ITelegratorBot -{ - private readonly IHost _innerHost; - private readonly IUpdateRouter _updateRouter; - private readonly ILogger _logger; - - private bool _disposed; - - /// - public IServiceProvider Services => _innerHost.Services; - - /// - public IUpdateRouter UpdateRouter => _updateRouter; - - /// - /// This application's logger - /// - public ILogger Logger => _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The proxied instance of host builder. - public TelegramBotHost(HostApplicationBuilder hostApplicationBuilder) - { - // Registering this host in services for easy access - hostApplicationBuilder.Services.AddSingleton(this); - - // Building proxy hoster - _innerHost = hostApplicationBuilder.Build(); - - // Reruesting services for this host - _updateRouter = Services.GetRequiredService(); - _logger = Services.GetRequiredService>(); - } - - /// - /// Creates new with default configuration, services and long-polling update receiving scheme - /// - /// - public static TelegramBotHostBuilder CreateBuilder() - { - HostApplicationBuilder innerBuilder = new HostApplicationBuilder(settings: null); - TelegramBotHostBuilder builder = new TelegramBotHostBuilder(innerBuilder); - return builder; - } - - /// - /// Creates new with default services and long-polling update receiving scheme - /// - /// - public static TelegramBotHostBuilder CreateBuilder(HostApplicationBuilderSettings? settings) - { - HostApplicationBuilder innerBuilder = new HostApplicationBuilder(settings); - TelegramBotHostBuilder builder = new TelegramBotHostBuilder(innerBuilder); - return builder; - } - - /// - /// Creates new EMPTY WITHOUT any services or update receiving schemes - /// - /// - public static TelegramBotHostBuilder CreateEmptyBuilder() - { - HostApplicationBuilder innerBuilder = Host.CreateEmptyApplicationBuilder(null); - return new TelegramBotHostBuilder(innerBuilder); - } - - /// - /// Creates new EMPTY WITHOUT any services or update receiving schemes - /// - /// - public static TelegramBotHostBuilder CreateEmptyBuilder(HostApplicationBuilderSettings? settings) - { - HostApplicationBuilder innerBuilder = Host.CreateEmptyApplicationBuilder(settings); - return new TelegramBotHostBuilder(innerBuilder); - } - - /// - public async Task StartAsync(CancellationToken cancellationToken = default) - { - await _innerHost.StartAsync(cancellationToken); - } - - /// - public async Task StopAsync(CancellationToken cancellationToken = default) - { - await _innerHost.StopAsync(cancellationToken); - } - - /// - /// Disposes the host. - /// - public void Dispose() - { - if (_disposed) - return; - - _innerHost.Dispose(); - - GC.SuppressFinalize(this); - _disposed = true; - } -} diff --git a/src/Telegrator.Hosting/Hosting/TelegramBotHostBuilder.cs b/src/Telegrator.Hosting/Hosting/TelegramBotHostBuilder.cs index faa72a5..6c409bb 100644 --- a/src/Telegrator.Hosting/Hosting/TelegramBotHostBuilder.cs +++ b/src/Telegrator.Hosting/Hosting/TelegramBotHostBuilder.cs @@ -11,7 +11,7 @@ namespace Telegrator.Hosting; /// public class TelegramBotHostBuilder : ITelegramBotHostBuilder { - private readonly HostApplicationBuilder _innerBuilder; + private readonly IHostApplicationBuilder _innerBuilder; internal IHandlersCollection _handlers = null!; /// @@ -30,7 +30,7 @@ public class TelegramBotHostBuilder : ITelegramBotHostBuilder public IHostEnvironment Environment => _innerBuilder.Environment; /// - public IDictionary Properties => ((IHostApplicationBuilder)_innerBuilder).Properties; + public IDictionary Properties => _innerBuilder.Properties; /// public IMetricsBuilder Metrics => _innerBuilder.Metrics; @@ -39,7 +39,7 @@ public class TelegramBotHostBuilder : ITelegramBotHostBuilder /// Initializes a new instance of the class. /// /// - public TelegramBotHostBuilder(HostApplicationBuilder hostApplicationBuilder) + public TelegramBotHostBuilder(IHostApplicationBuilder hostApplicationBuilder) { _innerBuilder = hostApplicationBuilder ?? throw new ArgumentNullException(nameof(hostApplicationBuilder)); } @@ -49,23 +49,12 @@ public class TelegramBotHostBuilder : ITelegramBotHostBuilder /// /// /// - public TelegramBotHostBuilder(HostApplicationBuilder hostApplicationBuilder, IHandlersCollection handlers) + public TelegramBotHostBuilder(IHostApplicationBuilder hostApplicationBuilder, IHandlersCollection handlers) { _innerBuilder = hostApplicationBuilder ?? throw new ArgumentNullException(nameof(hostApplicationBuilder)); _handlers = handlers ?? throw new ArgumentNullException(nameof(handlers)); } - /// - /// Builds the host. - /// - /// - public TelegramBotHost Build() - { - TelegramBotHost host = new TelegramBotHost(_innerBuilder); - host.UseTelegrator(); - return host; - } - /// public void ConfigureContainer(IServiceProviderFactory factory, Action? configure = null) where TContainerBuilder : notnull { diff --git a/src/Telegrator.Hosting/Polling/HostUpdateRouter.cs b/src/Telegrator.Hosting/Mediation/HostUpdateRouter.cs similarity index 97% rename from src/Telegrator.Hosting/Polling/HostUpdateRouter.cs rename to src/Telegrator.Hosting/Mediation/HostUpdateRouter.cs index a4cbcca..d8c2e61 100644 --- a/src/Telegrator.Hosting/Polling/HostUpdateRouter.cs +++ b/src/Telegrator.Hosting/Mediation/HostUpdateRouter.cs @@ -5,9 +5,8 @@ using Telegram.Bot.Polling; using Telegram.Bot.Types; using Telegrator.Core; using Telegrator.Core.States; -using Telegrator.Mediation; -namespace Telegrator.Polling; +namespace Telegrator.Mediation; /// public class HostUpdateRouter : UpdateRouter diff --git a/src/Telegrator.Hosting/Polling/HostedUpdateReceiver.cs b/src/Telegrator.Hosting/Mediation/HostedUpdateReceiver.cs similarity index 90% rename from src/Telegrator.Hosting/Polling/HostedUpdateReceiver.cs rename to src/Telegrator.Hosting/Mediation/HostedUpdateReceiver.cs index 06489b8..89d8f02 100644 --- a/src/Telegrator.Hosting/Polling/HostedUpdateReceiver.cs +++ b/src/Telegrator.Hosting/Mediation/HostedUpdateReceiver.cs @@ -4,9 +4,8 @@ using Microsoft.Extensions.Options; using Telegram.Bot; using Telegram.Bot.Polling; using Telegrator.Core; -using Telegrator.Mediation; -namespace Telegrator.Polling; +namespace Telegrator.Mediation; /// /// Service for receiving updates for Hosted telegram bots @@ -26,7 +25,8 @@ public class HostedUpdateReceiver(ITelegramBotClient botClient, IUpdateRouter up logger.LogInformation("Starting receiving updates via long-polling"); _receiverOptions.AllowedUpdates = _updateRouter.HandlersProvider.AllowedTypes.ToArray(); - botClient.DeleteWebhook(options.Value.DropPendingUpdates).Wait(); + botClient.DeleteWebhook(options.Value.DropPendingUpdates, cancellationToken: stoppingToken) + .ConfigureAwait(false).GetAwaiter().GetResult(); DefaultUpdateReceiver updateReceiver = new DefaultUpdateReceiver(botClient, _receiverOptions); await updateReceiver.ReceiveAsync(_updateRouter, stoppingToken).ConfigureAwait(false); diff --git a/src/Telegrator.Hosting/Providers/HostHandlersCollection.cs b/src/Telegrator.Hosting/Providers/HostHandlersCollection.cs index 8628050..4584fcf 100644 --- a/src/Telegrator.Hosting/Providers/HostHandlersCollection.cs +++ b/src/Telegrator.Hosting/Providers/HostHandlersCollection.cs @@ -17,41 +17,60 @@ public class HostHandlersCollection(IServiceCollection hostServiceColletion, Tel { switch (descriptor.Type) { + default: + throw new Exception("Unknown descriptor type"); + case DescriptorType.General: { if (descriptor.InstanceFactory != null) + { Services.AddScoped(descriptor.HandlerType, _ => descriptor.InstanceFactory.Invoke()); - else - Services.AddScoped(descriptor.HandlerType); + break; + } + Services.AddScoped(descriptor.HandlerType); break; } case DescriptorType.Keyed: { if (descriptor.InstanceFactory != null) + { Services.AddKeyedScoped(descriptor.HandlerType, descriptor.ServiceKey, (_, _) => descriptor.InstanceFactory.Invoke()); - else - Services.AddKeyedScoped(descriptor.HandlerType, descriptor.ServiceKey); + break; + } + Services.AddKeyedScoped(descriptor.HandlerType, descriptor.ServiceKey); break; } case DescriptorType.Singleton: { - Services.AddSingleton(descriptor.HandlerType, descriptor.SingletonInstance ?? (descriptor.InstanceFactory != null - ? descriptor.InstanceFactory.Invoke() - : throw new Exception())); + if (descriptor.SingletonInstance != null) + { + Services.AddSingleton(descriptor.HandlerType, descriptor.SingletonInstance); + break; + } + if (descriptor.InstanceFactory == null) + throw new InvalidOperationException("Singleton handler descriptor without singleton instance should implement `InstanceFactory`"); + + Services.AddSingleton(descriptor.HandlerType, descriptor.InstanceFactory.Invoke()); break; } case DescriptorType.Implicit: { - Services.AddKeyedSingleton(descriptor.HandlerType, descriptor.ServiceKey, descriptor.SingletonInstance ?? (descriptor.InstanceFactory != null - ? descriptor.InstanceFactory.Invoke() - : throw new Exception())); - + if (descriptor.SingletonInstance != null) + { + Services.AddKeyedSingleton(descriptor.HandlerType, descriptor.ServiceKey, descriptor.SingletonInstance); + break; + } + + if (descriptor.InstanceFactory == null) + throw new InvalidOperationException("Implicit handler descriptor without singleton instance should implement `InstanceFactory`"); + + Services.AddKeyedSingleton(descriptor.HandlerType, descriptor.ServiceKey, descriptor.InstanceFactory.Invoke()); break; } } diff --git a/src/Telegrator.Hosting/TypesExtensions.cs b/src/Telegrator.Hosting/TypesExtensions.cs index 0e5294a..c998be2 100644 --- a/src/Telegrator.Hosting/TypesExtensions.cs +++ b/src/Telegrator.Hosting/TypesExtensions.cs @@ -15,7 +15,7 @@ using Telegrator.Core.Descriptors; using Telegrator.Core.States; using Telegrator.Hosting; using Telegrator.Logging; -using Telegrator.Polling; +using Telegrator.Mediation; using Telegrator.Providers; using Telegrator.States; @@ -34,20 +34,7 @@ public static class HostBuilderExtensions /// /// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. /// - public static ITelegramBotHostBuilder AddTelegrator(this ITelegramBotHostBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null, Action? action = null) - { - AddTelegratorInternal(builder.Services, builder.Configuration, builder.Properties, ref handlers, options); - if (builder is TelegramBotHostBuilder telegramBotHostBuilder) - telegramBotHostBuilder._handlers = handlers; - - action?.Invoke(builder); - return builder; - } - - /// - /// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. - /// - public static IHostApplicationBuilder AddTelegrator(this HostApplicationBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null, Action? action = null) + public static IHostApplicationBuilder AddTelegrator(this IHostApplicationBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null, Action? action = null) { AddTelegratorInternal(builder.Services, builder.Configuration, ((IHostApplicationBuilder)builder).Properties, ref handlers, options); action?.Invoke(new TelegramBotHostBuilder(builder, handlers)); @@ -72,6 +59,16 @@ public static class HostBuilderExtensions return builder; } + /// + /// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. + /// + public static IHostBuilder AddTelegrator(this IHostBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null, Action? action = null) + { + builder.ConfigureServices((ctx, sp) => AddTelegratorInternal(sp, ctx.Configuration, builder.Properties, ref handlers, options)); + action?.Invoke(handlers!); // AddTelegratorInternal initializes `handlers` + return builder; + } + /// /// Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. /// @@ -81,7 +78,7 @@ public static class HostBuilderExtensions { options = configuration.GetSection(nameof(TelegratorOptions)).Get(); if (options == null) - throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegratorOptions' wasn't registered. This configuration is runtime required!"); + throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegratorOptions' was registered. This configuration is runtime required!"); } CancellationTokenSource globallCancell = new CancellationTokenSource(); @@ -103,11 +100,11 @@ public static class HostBuilderExtensions services.AddSingleton(handlers); properties.Add(HandlersCollectionPropertyKey, handlers); - if (!services.Any(srvc => srvc.ImplementationType == typeof(IOptions))) + if (!services.Any(srvc => srvc.ServiceType == typeof(IOptions))) { ReceiverOptions? receiverOptions = configuration.GetSection(nameof(ReceiverOptions)).Get(); if (receiverOptions == null) - throw new MissingMemberException("Auto configuration disabled, yet no options of type 'ReceiverOptions' wasn't registered. This configuration is runtime required!"); + throw new MissingMemberException("Auto configuration disabled, yet no options of type 'ReceiverOptions' was registered. This configuration is runtime required!"); services.AddSingleton(Options.Create(receiverOptions)); } @@ -132,6 +129,18 @@ public static class HostBuilderExtensions /// public static class ServicesCollectionExtensions { + public static IServiceCollection ConfigureTelegram(this IServiceCollection services, TelegramBotClientOptions options) + { + services.AddSingleton(Options.Create(options)); + return services; + } + + public static IServiceCollection ConfigureReceiver(this IServiceCollection services, ReceiverOptions options) + { + services.AddSingleton(Options.Create(options)); + return services; + } + /// /// Registers service /// @@ -145,7 +154,7 @@ public static class ServicesCollectionExtensions } /// - /// Registers default services + /// Registers default services /// /// /// @@ -188,7 +197,7 @@ public static class ServicesCollectionExtensions public static class TelegramBotHostExtensions { /// - /// Replaces the initialization logic from TelegramBotWebHost constructor. + /// Replaces the initialization logic from TelegramBotWebHost constructor. /// Initializes the bot and logs handlers on application startup. /// public static IHost UseTelegrator(this IHost botHost) @@ -200,11 +209,14 @@ public static class TelegramBotHostExtensions if (logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Information)) { - logger.LogInformation("Telegrator Bot .NET Host started"); + logger.LogInformation("Telegrator Bot Host started (Generic Host)"); + logger.LogInformation("Receiving mode : LONG-POLLING"); 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; } @@ -219,7 +231,12 @@ public static class TelegramBotHostExtensions IUpdateRouter router = botHost.Services.GetRequiredService(); IEnumerable aliases = router.HandlersProvider.GetBotCommands(); - client.SetMyCommands(aliases).Wait(); + if (aliases.Any()) + { + client.SetMyCommands(aliases) + .ConfigureAwait(false).GetAwaiter().GetResult(); + } + return botHost; } @@ -255,7 +272,7 @@ public static class LoggerExtensions StringBuilder logBuilder = new StringBuilder("Registered handlers : "); if (!handlers.Keys.Any()) - throw new Exception(); + throw new Exception("No update types were registered"); foreach (UpdateType updateType in handlers.Keys) { diff --git a/src/Telegrator/Core/Handlers/UpdateHandlerBase.cs b/src/Telegrator/Core/Handlers/UpdateHandlerBase.cs index 850826e..9997296 100644 --- a/src/Telegrator/Core/Handlers/UpdateHandlerBase.cs +++ b/src/Telegrator/Core/Handlers/UpdateHandlerBase.cs @@ -56,7 +56,7 @@ public abstract class UpdateHandlerBase(UpdateType handlingUpdateType) : IUpdate .ExecutePre(this, container, cancellationToken) .ConfigureAwait(false); - if (!preResult.Positive) + if (!preResult.InterruptRouter) return preResult; } catch (NotImplementedException) @@ -69,7 +69,7 @@ public abstract class UpdateHandlerBase(UpdateType handlingUpdateType) : IUpdate { // Executing handler Result execResult = await ExecuteInternal(container, cancellationToken).ConfigureAwait(false); - if (!execResult.Positive) + if (!execResult.InterruptRouter) return execResult; } catch (NotImplementedException) @@ -86,7 +86,7 @@ public abstract class UpdateHandlerBase(UpdateType handlingUpdateType) : IUpdate .ExecutePost(this, container, cancellationToken) .ConfigureAwait(false); - if (!postResult.Positive) + if (!postResult.InterruptRouter) return postResult; } } diff --git a/src/Telegrator/Mediation/UpdateRouter.cs b/src/Telegrator/Mediation/UpdateRouter.cs index 9bbd876..7be110c 100644 --- a/src/Telegrator/Mediation/UpdateRouter.cs +++ b/src/Telegrator/Mediation/UpdateRouter.cs @@ -19,27 +19,22 @@ namespace Telegrator.Mediation; /// public class UpdateRouter : IUpdateRouter { - private readonly TelegratorOptions _options; - private readonly IHandlersProvider _handlersProvider; - private readonly IAwaitingProvider _awaitingProvider; - private readonly IStateStorage _stateStorage; - private readonly IUpdateHandlersPool _HandlersPool; private readonly ITelegramBotInfo _botInfo; /// - public IHandlersProvider HandlersProvider => _handlersProvider; + public TelegratorOptions Options { get; } /// - public IAwaitingProvider AwaitingProvider => _awaitingProvider; + public IHandlersProvider HandlersProvider { get; } /// - public IStateStorage StateStorage => _stateStorage; + public IAwaitingProvider AwaitingProvider { get; } /// - public TelegratorOptions Options => _options; + public IStateStorage StateStorage { get; } /// - public IUpdateHandlersPool HandlersPool => _HandlersPool; + public IUpdateHandlersPool HandlersPool { get; } /// public IRouterExceptionHandler? ExceptionHandler { get; set; } @@ -57,11 +52,11 @@ public class UpdateRouter : IUpdateRouter /// public UpdateRouter(IHandlersProvider handlersProvider, IAwaitingProvider awaitingProvider, IStateStorage stateStorage, TelegratorOptions options, ITelegramBotInfo botInfo) { - _options = options; - _handlersProvider = handlersProvider; - _awaitingProvider = awaitingProvider; - _stateStorage = stateStorage; - _HandlersPool = new UpdateHandlersPool(this, _options, _options.GlobalCancellationToken); + Options = options; + HandlersProvider = handlersProvider; + AwaitingProvider = awaitingProvider; + StateStorage = stateStorage; + HandlersPool = new UpdateHandlersPool(this, Options, Options.GlobalCancellationToken); _botInfo = botInfo; } @@ -116,7 +111,7 @@ public class UpdateRouter : IUpdateRouter if (lastResult == null) break; // Smth went horribly wrong, better to stop routing - if (lastResult != null && !lastResult.RouteNext) + if (lastResult.InterruptRouter) break; TelegratorLogging.LogTrace("Handler '{0}' requested route continuation (Update {1})", handlerInfo.DisplayString, handlerInfo.HandlingUpdate.Id); @@ -146,7 +141,7 @@ public class UpdateRouter : IUpdateRouter if (lastResult == null) break; // Smth went horribly wrong, better to stop routing - if (lastResult != null && !lastResult.RouteNext) + if (lastResult.InterruptRouter) break; TelegratorLogging.LogTrace("Handler '{0}' requested route continuation (Update {1})", handlerInfo.DisplayString, handlerInfo.HandlingUpdate.Id); @@ -248,17 +243,13 @@ public class UpdateRouter : IUpdateRouter { FiltersFallbackReport report = new FiltersFallbackReport(descriptor, filterContext); Result filtersResult = descriptor.Filters.Validate(filterContext, descriptor.FormReport, ref report); + + if (filtersResult.InterruptRouter) + return null; - if (filtersResult.RouteNext) - { - Result fallbackResult = handlerInstance.FiltersFallback(report, client, cancellationToken).Result; - breakRouting = !fallbackResult.RouteNext; - return null; - } - else if (!filtersResult.Positive) - { - return null; - } + Result fallbackResult = handlerInstance.FiltersFallback(report, client, cancellationToken).Result; + breakRouting = fallbackResult.InterruptRouter; + return null; } return new DescribedHandlerDescriptor(descriptor, this, AwaitingProvider, StateStorage, client, handlerInstance, filterContext, descriptor.DisplayString); @@ -307,6 +298,4 @@ public class UpdateRouter : IUpdateRouter TelegratorLogging.LogTrace(sb.ToString()); } - - private class BreakDescribingException : Exception { } } diff --git a/src/Telegrator/Result.cs b/src/Telegrator/Result.cs index ddadd12..6276ab0 100644 --- a/src/Telegrator/Result.cs +++ b/src/Telegrator/Result.cs @@ -10,29 +10,23 @@ namespace Telegrator; /// public sealed class Result { - private static readonly Result ok = new Result(true, false, null); - private static readonly Result fault = new Result(false, false, null); - private static readonly Result next = new Result(true, true, null); + private static readonly Result ok = new Result(true, null); + private static readonly Result fault = new Result(true, null); + private static readonly Result next = new Result(false, null); /// - /// Is result positive + /// Tell router to stop describing /// - public bool Positive { get; } - - /// - /// Should router search for next matching handler - /// - public bool RouteNext { get; } + public bool InterruptRouter { get; } /// /// Exact type that router should search /// public Type? NextType { get; } - internal Result(bool positive, bool routeNext, Type? nextType) + internal Result(bool interruptRouter, Type? nextType) { - Positive = positive; - RouteNext = routeNext; + InterruptRouter = interruptRouter; NextType = nextType; } @@ -79,5 +73,5 @@ public sealed class Result /// /// public static Result Next() - => new Result(true, true, typeof(T)); + => new Result(false, typeof(T)); } diff --git a/src/Telegrator/SimpleTypesExtensions.cs b/src/Telegrator/SimpleTypesExtensions.cs index 98aeea9..0c00229 100644 --- a/src/Telegrator/SimpleTypesExtensions.cs +++ b/src/Telegrator/SimpleTypesExtensions.cs @@ -26,6 +26,21 @@ public static partial class ColletionsExtensions return new ReadOnlyDictionary(dictionary); } + /// + /// Remove all values and returns collection without nullable type. + /// + /// + /// + /// + public static IEnumerable Squeeze(this IEnumerable source) + { + foreach (T? item in source) + { + if (item is not null) + yield return item; + } + } + /// /// Enumerates objects in a and executes an on each one /// @@ -188,7 +203,7 @@ public static partial class ReflectionExtensions /// /// /// - public static bool IsHandlerRealization(this Type type) + public static bool IsHandlerImplementation(this Type type) => !type.IsAbstract && type != typeof(UpdateHandlerBase) && typeof(UpdateHandlerBase).IsAssignableFrom(type); /// @@ -260,6 +275,7 @@ public static partial class StringExtensions { char[] chars = target.ToCharArray(); int index = chars.IndexOf(char.IsLetter); + chars[index] = char.ToUpper(chars[index]); return new string(chars); } @@ -273,6 +289,7 @@ public static partial class StringExtensions { char[] chars = target.ToCharArray(); int index = chars.IndexOf(char.IsLetter); + chars[index] = char.ToLower(chars[index]); return new string(chars); } diff --git a/src/Telegrator/TelegratorClient.cs b/src/Telegrator/TelegratorClient.cs index 05d6f1f..ec16e3f 100644 --- a/src/Telegrator/TelegratorClient.cs +++ b/src/Telegrator/TelegratorClient.cs @@ -29,7 +29,7 @@ public class TelegratorClient : TelegramBotClient, ITelegratorBot, ICollectingPr public ITelegramBotInfo BotInfo { get; private set; } /// - public IUpdateRouter UpdateRouter { get => updateRouter ?? throw new Exception(); } + public IUpdateRouter UpdateRouter => updateRouter ?? throw new InvalidOperationException("Router's not created yet. Invoke `StartReceiving` to initialize this property."); /// /// Initializes a new instance of the class with a bot token. @@ -68,11 +68,11 @@ public class TelegratorClient : TelegramBotClient, ITelegratorBot, ICollectingPr /// Initializes the update router and begins polling for updates. /// /// Optional receiver options for configuring update polling. - /// The cancellation token to stop receiving updates. - public void StartReceiving(ReceiverOptions? receiverOptions = null, CancellationToken cancellationToken = default) + /// The cancellation token to stop receiving updates. + public void StartReceiving(ReceiverOptions? receiverOptions = null, CancellationToken globalCancellationToken = default) { if (Options.GlobalCancellationToken == CancellationToken.None) - Options.GlobalCancellationToken = cancellationToken; + Options.GlobalCancellationToken = globalCancellationToken; HandlersProvider handlerProvider = new HandlersProvider(Handlers, Options); AwaitingProvider awaitingProvider = new AwaitingProvider(Options); @@ -82,8 +82,7 @@ public class TelegratorClient : TelegramBotClient, ITelegratorBot, ICollectingPr // Log startup TelegratorLogging.LogInformation($"Telegrator bot starting up - BotId: {BotInfo.User.Id}, Username: {BotInfo.User.Username}, MaxParallelHandlers: {Options.MaximumParallelWorkingHandlers ?? -1}"); - - StartReceivingInternal(receiverOptions, cancellationToken); + StartReceivingInternal(receiverOptions, globalCancellationToken); } /// @@ -115,6 +114,4 @@ public class TelegratorClient : TelegramBotClient, ITelegratorBot, ICollectingPr TelegratorLogging.LogInformation("Telegrator bot stopped (cancelled)"); } } - - } diff --git a/src/Telegrator/TypesExtensions.cs b/src/Telegrator/TypesExtensions.cs index b6e2904..93db750 100644 --- a/src/Telegrator/TypesExtensions.cs +++ b/src/Telegrator/TypesExtensions.cs @@ -412,6 +412,26 @@ public static partial class HandlersCollectionExtensions "Ocelot", "BouncyCastle", "IdentityModel", "Telegrator" ]; + /// + /// Collects all handlers from current app domain. + /// Scans for handlers exported by analyzer into class `Telegrator.Analyzers.AnalyzerExport` in each assembly and registers them to the collection. + /// + /// + /// + public static IHandlersCollection CollectHandlers(this IHandlersCollection handlers) + { + const string exportClassName = "Telegrator.Analyzers.AnalyzerExport"; + AppDomain.CurrentDomain.GetAssemblies() + .Select(ass => ass.GetType(exportClassName)) + .Squeeze() + .SelectMany(t => t.GetFields()) + .Select(f => f.GetValue(null) as Type) + .Squeeze() + .ForEach(v => handlers.AddHandler(v)); + + return handlers; + } + /// /// Collects all public handlers from the current app domain. /// Scans for types that implement handlers and adds them to the collection. @@ -438,7 +458,7 @@ public static partial class HandlersCollectionExtensions { (collectingTarget ?? Assembly.GetCallingAssembly()) .GetExportedTypes() - .Where(type => type.GetCustomAttribute() == null && type.IsHandlerRealization()) + .Where(type => type.GetCustomAttribute() == null && type.IsHandlerImplementation()) .ForEach(type => handlers.AddHandler(type)); return handlers; @@ -496,7 +516,7 @@ public static partial class HandlersCollectionExtensions /// Thrown when the type is not a valid handler implementation. public static IHandlersCollection AddHandler(this IHandlersCollection handlers, Type handlerType) { - if (!handlerType.IsHandlerRealization()) + if (!handlerType.IsHandlerImplementation()) throw new Exception(); if (handlerType.IsCustomDescriptorsProvider())