diff --git a/docs/Telegrator.Hosting.WideBot.xml b/docs/Telegrator.Hosting.WideBot.xml index 3df7372..b48e7d6 100644 --- a/docs/Telegrator.Hosting.WideBot.xml +++ b/docs/Telegrator.Hosting.WideBot.xml @@ -19,7 +19,7 @@ Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. - + Replaces TelegramBotHostBuilder. Configures DI, options, and handlers. diff --git a/docs/Telegrator.xml b/docs/Telegrator.xml index d43eb31..f8b3a28 100644 --- a/docs/Telegrator.xml +++ b/docs/Telegrator.xml @@ -6377,11 +6377,16 @@ Represents handler results, allowing to communicate with router and control aspect execution - + Tell router to stop describing + + + Tell router to continue describing + + Exact type that router should search diff --git a/src/Telegrator.Hosting.WideBot/TypesExtensions.cs b/src/Telegrator.Hosting.WideBot/TypesExtensions.cs index 4cb188e..fa6c003 100644 --- a/src/Telegrator.Hosting.WideBot/TypesExtensions.cs +++ b/src/Telegrator.Hosting.WideBot/TypesExtensions.cs @@ -4,12 +4,7 @@ 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; @@ -58,6 +53,12 @@ public static class HandlersExtensions } } + public static WTelegramBotClient AsWClient(this ITelegramBotClient client) + { + return client as WTelegramBotClient + ?? throw new InvalidCastException("Client is not assignable to `WTelegram.Bot.WTelegramBotClient`"); + } + public static WUpdate AsWUpdate(this Update update) { return update as WUpdate @@ -75,7 +76,7 @@ public static class WideHostBuilderExtensions /// 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); + AddWideTelegratorInternal(builder.Services, builder.Configuration, builder.Properties, ref handlers, options); action?.Invoke(new TelegramBotHostBuilder(builder, handlers)); return builder; } @@ -85,14 +86,14 @@ public static class WideHostBuilderExtensions /// public static IHostApplicationBuilder AddWideTelegrator(this IHostApplicationBuilder builder, TelegratorOptions? options = null, IHandlersCollection? handlers = null) { - AddTelegratorInternal(builder.Services, builder.Configuration, builder.Properties, ref handlers, options); + AddWideTelegratorInternal(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) + internal static void AddWideTelegratorInternal(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."); @@ -123,7 +124,7 @@ public static class WideHostBuilderExtensions services.AddSingleton(handlers); properties.Add(HostBuilderExtensions.HandlersCollectionPropertyKey, handlers); - if (!services.Any(srvc => srvc.ImplementationType == typeof(IOptions))) + if (!services.Any(srvc => srvc.ServiceType == 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."); @@ -163,7 +164,13 @@ public static class WideBotServiceCollectionExtensions } private static WTelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider) - => new WTelegramBotClient(provider.GetRequiredService>().Value, httpClient); + { + ILogger logger = provider.GetRequiredService>(); + WTelegramBotClient client = new WTelegramBotClient(provider.GetRequiredService>().Value, httpClient); + + WTelegram.Helpers.Log = (lvl, str) => logger.Log((LogLevel)lvl, str); + return client; + } } /// diff --git a/src/Telegrator/Core/Descriptors/DescriptorFiltersSet.cs b/src/Telegrator/Core/Descriptors/DescriptorFiltersSet.cs index c4c7c66..121064b 100644 --- a/src/Telegrator/Core/Descriptors/DescriptorFiltersSet.cs +++ b/src/Telegrator/Core/Descriptors/DescriptorFiltersSet.cs @@ -125,7 +125,8 @@ public sealed class DescriptorFiltersSet if (!anyErrors) return Result.Ok(); - return formReport ? Result.Next() : Result.Fault(); + return formReport + ? Result.Next() : Result.Fault(); } private static bool ExecuteFilter(IFilter filter, FilterExecutionContext context, out Exception? exception) where T : class diff --git a/src/Telegrator/Core/Descriptors/HandlerInspector.cs b/src/Telegrator/Core/Descriptors/HandlerInspector.cs index 25c9d77..1ded5fb 100644 --- a/src/Telegrator/Core/Descriptors/HandlerInspector.cs +++ b/src/Telegrator/Core/Descriptors/HandlerInspector.cs @@ -1,4 +1,7 @@ -using System.ComponentModel; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using System.Reflection; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; @@ -21,6 +24,11 @@ public static class HandlerInspector /// public static string? GetDisplayName(MemberInfo handlerType) { + if (handlerType == null) + { + throw new ArgumentNullException(nameof(handlerType)); + } + return handlerType.GetCustomAttribute()?.DisplayName; } @@ -31,11 +39,26 @@ public static class HandlerInspector /// The handler attribute. public static UpdateHandlerAttributeBase GetHandlerAttribute(MemberInfo handlerType) { - // Getting polling handler attribute - IEnumerable handlerAttrs = handlerType.GetCustomAttributes(); + if (handlerType == null) + { + throw new ArgumentNullException(nameof(handlerType)); + } - // - return handlerAttrs.Single(); + List handlerAttrs = handlerType.GetCustomAttributes().ToList(); + + if (handlerAttrs.Count == 0) + { + throw new InvalidOperationException( + $"Failed to register handler '{handlerType.Name}': Missing required attribute derived from '{nameof(UpdateHandlerAttributeBase)}'."); + } + + if (handlerAttrs.Count > 1) + { + throw new InvalidOperationException( + $"Failed to register handler '{handlerType.Name}': Multiple handler attributes found. A handler must have exactly one attribute derived from '{nameof(UpdateHandlerAttributeBase)}'."); + } + + return handlerAttrs[0]; } /// @@ -45,10 +68,16 @@ public static class HandlerInspector /// The state keeper attribute, or null if not present. public static IFilter? GetStateKeeperAttribute(MemberInfo handlerType) { - // Getting polling handler attribute - Attribute stateAttr = handlerType.GetCustomAttribute(typeof(StateAttribute<,>)); + if (handlerType == null) + throw new ArgumentNullException(nameof(handlerType)); + + Attribute? stateAttr = handlerType.GetCustomAttributes() + .FirstOrDefault(attr => + { + Type type = attr.GetType(); + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(StateAttribute<,>); + }); - // return stateAttr as IFilter; } @@ -60,12 +89,19 @@ public static class HandlerInspector /// An enumerable of filter attributes. public static IEnumerable> GetFilterAttributes(MemberInfo handlerType, UpdateType validUpdType) { - // - IEnumerable filters = handlerType.GetCustomAttributes(); + if (handlerType == null) + throw new ArgumentNullException(nameof(handlerType)); - // - if (filters.Any(filterAttr => !filterAttr.AllowedTypes.Contains(validUpdType))) - throw new InvalidOperationException(); + List filters = handlerType.GetCustomAttributes().ToList(); + + UpdateFilterAttributeBase? invalidFilter = filters.FirstOrDefault(f => !f.AllowedTypes.Contains(validUpdType)); + if (invalidFilter != null) + { + string allowedTypesStr = string.Join(", ", invalidFilter.AllowedTypes); + throw new InvalidOperationException( + $"Filter conflict on handler '{handlerType.Name}': The filter '{invalidFilter.GetType().Name}' " + + $"does not support update type '{validUpdType}'. Allowed types: [{allowedTypesStr}]."); + } UpdateFilterAttributeBase? lastFilterAttribute = null; foreach (UpdateFilterAttributeBase filterAttribute in filters) @@ -78,7 +114,6 @@ public static class HandlerInspector else { lastFilterAttribute = filterAttribute; - continue; } } } @@ -91,8 +126,24 @@ public static class HandlerInspector /// A containing the aspects configuration. public static DescriptorAspectsSet GetAspects(Type handlerType) { - Type? typedPre = handlerType.GetCustomAttribute(typeof(BeforeExecutionAttribute<>))?.GetType().GetGenericArguments()[0]; - Type? typedPost = handlerType.GetCustomAttribute(typeof(AfterExecutionAttribute<>))?.GetType().GetGenericArguments()[0]; + if (handlerType == null) + throw new ArgumentNullException(nameof(handlerType)); + + Type? typedPre = GetGenericArgumentFromOpenGenericAttribute(handlerType, typeof(BeforeExecutionAttribute<>)); + Type? typedPost = GetGenericArgumentFromOpenGenericAttribute(handlerType, typeof(AfterExecutionAttribute<>)); + return new DescriptorAspectsSet(typedPre, typedPost); } -} + + private static Type? GetGenericArgumentFromOpenGenericAttribute(Type handlerType, Type openGenericAttributeType) + { + Attribute? attribute = handlerType.GetCustomAttributes() + .FirstOrDefault(attr => + { + Type type = attr.GetType(); + return type.IsGenericType && type.GetGenericTypeDefinition() == openGenericAttributeType; + }); + + return attribute?.GetType().GetGenericArguments().FirstOrDefault(); + } +} \ No newline at end of file diff --git a/src/Telegrator/Core/Handlers/UpdateHandlerBase.cs b/src/Telegrator/Core/Handlers/UpdateHandlerBase.cs index 9997296..9ef559e 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.InterruptRouter) + if (!preResult.Success) 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.InterruptRouter) + if (!execResult.Success) return execResult; } catch (NotImplementedException) @@ -86,7 +86,7 @@ public abstract class UpdateHandlerBase(UpdateType handlingUpdateType) : IUpdate .ExecutePost(this, container, cancellationToken) .ConfigureAwait(false); - if (!postResult.InterruptRouter) + if (!postResult.Success) return postResult; } } @@ -166,7 +166,7 @@ public abstract class UpdateHandlerBase(UpdateType handlingUpdateType) : IUpdate /// public virtual Task FiltersFallback(FiltersFallbackReport report, ITelegramBotClient client, CancellationToken cancellationToken = default) { - return Task.FromResult(Result.Ok()); + return Task.FromResult(Result.Next()); } /// diff --git a/src/Telegrator/Mediation/UpdateRouter.cs b/src/Telegrator/Mediation/UpdateRouter.cs index 7be110c..dfaee10 100644 --- a/src/Telegrator/Mediation/UpdateRouter.cs +++ b/src/Telegrator/Mediation/UpdateRouter.cs @@ -97,11 +97,8 @@ public class UpdateRouter : IUpdateRouter Result? lastResult = null; foreach (DescribedHandlerDescriptor handlerInfo in GetHandlers(AwaitingProvider, botClient, update, cancellationToken)) { - if (lastResult?.NextType != null) - { - if (lastResult.NextType != handlerInfo.From.HandlerType) - continue; - } + if (lastResult?.NextType is not null && lastResult?.NextType != handlerInfo.From.HandlerType) + continue; // Enqueuing found awiting handlers await HandlersPool.Enqueue(handlerInfo); @@ -111,7 +108,7 @@ public class UpdateRouter : IUpdateRouter if (lastResult == null) break; // Smth went horribly wrong, better to stop routing - if (lastResult.InterruptRouter) + if (!lastResult.RouteNext) break; TelegratorLogging.LogTrace("Handler '{0}' requested route continuation (Update {1})", handlerInfo.DisplayString, handlerInfo.HandlingUpdate.Id); @@ -127,11 +124,8 @@ public class UpdateRouter : IUpdateRouter // Queuing reagular handlers for execution foreach (DescribedHandlerDescriptor handlerInfo in GetHandlers(HandlersProvider, botClient, update, cancellationToken)) { - if (lastResult?.NextType != null) - { - if (lastResult.NextType != handlerInfo.From.HandlerType) - continue; - } + if (lastResult?.NextType is not null && lastResult?.NextType != handlerInfo.From.HandlerType) + continue; // Enqueuing found handlers await HandlersPool.Enqueue(handlerInfo); @@ -141,7 +135,7 @@ public class UpdateRouter : IUpdateRouter if (lastResult == null) break; // Smth went horribly wrong, better to stop routing - if (lastResult.InterruptRouter) + if (!lastResult.RouteNext) break; TelegratorLogging.LogTrace("Handler '{0}' requested route continuation (Update {1})", handlerInfo.DisplayString, handlerInfo.HandlingUpdate.Id); @@ -243,13 +237,17 @@ 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; - Result fallbackResult = handlerInstance.FiltersFallback(report, client, cancellationToken).Result; - breakRouting = fallbackResult.InterruptRouter; - return null; + if (filtersResult.RouteNext) + { + Result fallbackResult = handlerInstance.FiltersFallback(report, client, cancellationToken).Result; + breakRouting = !fallbackResult.RouteNext; + return null; + } + else if (!filtersResult.Success) + { + return null; + } } return new DescribedHandlerDescriptor(descriptor, this, AwaitingProvider, StateStorage, client, handlerInstance, filterContext, descriptor.DisplayString); diff --git a/src/Telegrator/Result.cs b/src/Telegrator/Result.cs index 6276ab0..dd94060 100644 --- a/src/Telegrator/Result.cs +++ b/src/Telegrator/Result.cs @@ -10,23 +10,29 @@ namespace Telegrator; /// public sealed class Result { - 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); + 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); /// /// Tell router to stop describing /// - public bool InterruptRouter { get; } + public bool Success { get; } + + /// + /// Tell router to continue describing + /// + public bool RouteNext { get; } /// /// Exact type that router should search /// public Type? NextType { get; } - internal Result(bool interruptRouter, Type? nextType) + internal Result(bool success, bool routeNext, Type? nextType) { - InterruptRouter = interruptRouter; + Success = success; + RouteNext = routeNext; NextType = nextType; } @@ -73,5 +79,5 @@ public sealed class Result /// /// public static Result Next() - => new Result(false, typeof(T)); + => new Result(true, true, typeof(T)); } diff --git a/src/Telegrator/Telegrator.csproj b/src/Telegrator/Telegrator.csproj index 4d23723..3323936 100644 --- a/src/Telegrator/Telegrator.csproj +++ b/src/Telegrator/Telegrator.csproj @@ -32,7 +32,7 @@ - + diff --git a/tests/Telegrator.Tests/Program.cs b/tests/Telegrator.Tests/Program.cs index be28125..f8870ed 100644 --- a/tests/Telegrator.Tests/Program.cs +++ b/tests/Telegrator.Tests/Program.cs @@ -2,28 +2,12 @@ using Microsoft.Data.Sqlite; using Microsoft.Extensions.Hosting; using System.Data.Common; +using Telegram.Bot; -namespace Telegrator; +namespace Telegrator.Tests; internal static class Program { - public static void WebApplicationBuilder_Example(string[] args) - { - WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions() - { - Args = args, - ApplicationName = "WebApplication example", - }); - - builder.AddTelegratorWeb(action: builder => builder.Handlers - .CollectHandlersAssemblyWide()); - - builder.Build() - .UseTelegratorWeb(dontMap: true) - .RemapWebhook("https://amazing-butt-sex.cloudpub.ru/") - .Run(); - } - public static void HostApplicationBuilder_Example(string[] args) { HostApplicationBuilder builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings() @@ -49,7 +33,8 @@ internal static class Program }); using DbConnection connection = new SqliteConnection(@"Data Source=wtgb.db"); - builder.Services.ConfigureWideTelegram(new Telegram.Bot.WTelegramBotClientOptions(token: "BOT_TOKEN", apiId: 123, apiHash: "API_HASH", dbConnection: connection)); + builder.Services.ConfigureWideTelegram( + new WTelegramBotClientOptions(token: "BOT_TOKEN", apiId: 123, apiHash: "API_HASH", dbConnection: connection)); builder.AddWideTelegrator(action: builder => builder.Handlers .CollectHandlersAssemblyWide()); @@ -58,4 +43,21 @@ internal static class Program .UseWideTelegrator() .Run(); } + + public static void WebApplicationBuilder_Example(string[] args) + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions() + { + Args = args, + ApplicationName = "WebApplication example", + }); + + builder.AddTelegratorWeb(action: builder => builder.Handlers + .CollectHandlersAssemblyWide()); + + builder.Build() + .UseTelegratorWeb(dontMap: true) + .RemapWebhook("https://amazing-butt-sex.cloudpub.ru/") + .Run(); + } }