< Summary

Information
Class: TeleFlow.Telegram.Internal.Handlers.TelegramHandlerDescriptorBuilder
Assembly: TeleFlow.Framework
File(s): /_/src/TeleFlow.Framework/Internal/Handlers/TelegramHandlerDescriptorBuilder.cs
Line coverage
86%
Covered lines: 628
Uncovered lines: 101
Coverable lines: 729
Total lines: 1395
Line coverage: 86.1%
Branch coverage
83%
Covered branches: 339
Total branches: 405
Branch coverage: 83.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Build(...)83.33%121292%
GetCandidateMethods(...)83.33%6687.5%
GetDeclaredCandidateMethods(...)100%11100%
HasInheritedHandlerMethods(...)100%11100%
HasRouteAttributes(...)100%2020100%
BuildForMethod(...)87.2%1158684.25%
BuildFilters(...)100%11100%
BuildRoleRequirements(...)100%11100%
BuildAutoAnswerCallback(...)90%101085.71%
NormalizeAutoAnswerText(...)75%4480%
AppendFilters(...)100%1818100%
AppendMarkerFilters(...)100%44100%
GetCustomFilters()100%88100%
TryGetTelegramFilterAttributeFilterType(...)100%66100%
ValidateCustomFilters(...)66.66%642458.97%
GetCustomFilterContextTypes(...)62.5%88100%
SupportsContextType(...)100%22100%
GetModuleName(...)100%22100%
IsClassBasedHandlerType(...)100%44100%
ValidateClassBasedHandlerCompatibility(...)61.53%952653.33%
GetCallbackHandlerPayloadType(...)83.33%6680%
GetRouteAttribute(...)100%22100%
GetRouteAttributes(...)100%22100%
BuildMessageRouteDefinitions(...)100%1414100%
CreateCallbackRouteDefinition(...)100%11100%
BuildChatMemberRouteDefinitions(...)100%44100%
BuildChatMemberTransitions(...)100%11100%
GetChatMemberTransitionAttributes(...)100%11100%
GetChatMemberChangedAttributes(...)100%11100%
HasChatMemberTransitionAttributes(...)50%22100%
HasStateAttributes(...)100%66100%
HasGenericStateAttributes(...)50%22100%
ResolveSceneStepState(...)80%111082.35%
GetSceneStateSegment(...)75%44100%
ToCamelCase(...)50%22100%
ResolveRouteValueTypes(...)79.16%452466.66%
BuildParameterDescriptors(...)90.47%484285.1%
CreateCommandPolicy(...)100%11100%
CreateEmptyRouteValues()100%11100%
GetRegexRouteValueNames(...)100%1177.77%
GetGenericCallbackPayloadType(...)87.5%8886.95%
HasGenericCallbackAttribute(...)100%22100%
ValidateCallbackPayloadType(...)62.5%10866.66%
IsCallbackDataPayloadType(...)100%11100%
ValidateReturnType(...)100%44100%
NormalizeCommand(...)50%8662.5%
FormatHandlerKind(...)0%3050%
NormalizeCommandPattern(...)100%22100%
TryGetRouteValueParameterShape(...)100%88100%
CreateSignatureException(...)50%22100%

File(s)

/_/src/TeleFlow.Framework/Internal/Handlers/TelegramHandlerDescriptorBuilder.cs

#LineLine coverage
 1using System.Reflection;
 2using System.Text.RegularExpressions;
 3using TeleFlow.Annotations;
 4using TeleFlow.Framework.States;
 5
 6namespace TeleFlow.Telegram.Internal.Handlers;
 7
 8internal static class TelegramHandlerDescriptorBuilder
 9{
 110    private static readonly NullabilityInfoContext NullabilityInfoContext = new();
 11
 12    public static IReadOnlyList<TelegramHandlerDescriptor> Build(
 13        Type handlerType,
 14        int firstRegistrationOrder)
 15    {
 35616        ArgumentNullException.ThrowIfNull(handlerType);
 17
 35618        if (handlerType.IsAbstract || handlerType.IsInterface)
 19        {
 020            throw new InvalidOperationException(
 021                $"Telegram handler type '{handlerType.FullName}' must be a concrete class.");
 22        }
 23
 35624        var registrationOrder = firstRegistrationOrder;
 35625        var descriptors = new List<TelegramHandlerDescriptor>();
 35626        var moduleName = GetModuleName(handlerType);
 35627        var isClassBasedHandler = IsClassBasedHandlerType(handlerType);
 28
 35629        if (!isClassBasedHandler && HasRouteAttributes(handlerType))
 30        {
 131            throw new InvalidOperationException(
 132                $"Telegram handler type '{handlerType.FullName}' declares class-level route metadata but does not derive
 33        }
 34
 140535        foreach (var method in GetCandidateMethods(handlerType, isClassBasedHandler))
 36        {
 35837            var methodDescriptors = BuildForMethod(
 35838                handlerType,
 35839                method,
 35840                registrationOrder,
 35841                moduleName,
 35842                includeClassRouteAttributes: isClassBasedHandler);
 43
 33844            descriptors.AddRange(methodDescriptors);
 33845            registrationOrder += methodDescriptors.Count;
 46        }
 47
 33448        if (HasInheritedHandlerMethods(handlerType))
 49        {
 250            throw new InvalidOperationException(
 251                $"Telegram handler type '{handlerType.FullName}' inherits handler methods. " +
 252                "Inherited handler methods must be overridden in the concrete handler type for generated registration pa
 53        }
 54
 33255        return descriptors;
 56    }
 57
 58    private static IEnumerable<MethodInfo> GetCandidateMethods(
 59        Type handlerType,
 60        bool isClassBasedHandler)
 61    {
 35562        if (!isClassBasedHandler)
 63        {
 34664            return GetDeclaredCandidateMethods(handlerType);
 65        }
 66
 967        var handleMethods = GetDeclaredCandidateMethods(handlerType)
 868            .Where(static method => string.Equals(method.Name, "HandleAsync", StringComparison.Ordinal))
 969            .ToArray();
 70
 971        if (handleMethods.Length != 1)
 72        {
 173            throw new InvalidOperationException(
 174                $"Class-based Telegram handler type '{handlerType.FullName}' must declare exactly one public instance Ha
 75        }
 76
 877        var routeMethods = GetDeclaredCandidateMethods(handlerType)
 878            .Where(static method => !string.Equals(method.Name, "HandleAsync", StringComparison.Ordinal))
 879            .Where(HasRouteAttributes)
 880            .ToArray();
 81
 882        if (routeMethods.Length > 0)
 83        {
 084            throw new InvalidOperationException(
 085                $"Class-based Telegram handler type '{handlerType.FullName}' must declare route metadata on the type or 
 86        }
 87
 888        return handleMethods;
 89    }
 90
 91    private static IEnumerable<MethodInfo> GetDeclaredCandidateMethods(Type handlerType)
 92    {
 36393        return handlerType
 36394            .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)
 36695            .Where(static method => !method.IsSpecialName)
 72996            .OrderBy(static method => method.MetadataToken);
 97    }
 98
 99    private static bool HasInheritedHandlerMethods(Type handlerType)
 100    {
 334101        return handlerType
 334102            .GetMethods(BindingFlags.Instance | BindingFlags.Public)
 1668103            .Where(method => method.DeclaringType != handlerType)
 334104            .Any(HasRouteAttributes);
 105    }
 106
 107    private static bool HasRouteAttributes(MemberInfo member)
 108    {
 1677109        return member.GetCustomAttributes<CommandAttribute>(inherit: true).Any() ||
 1677110               member.GetCustomAttributes<MessageAttribute>(inherit: true).Any() ||
 1677111               member.GetCustomAttributes<TextAttribute>(inherit: true).Any() ||
 1677112               member.GetCustomAttributes<TextTemplateAttribute>(inherit: true).Any() ||
 1677113               member.GetCustomAttributes<CommandTemplateAttribute>(inherit: true).Any() ||
 1677114               member.GetCustomAttributes<TextRegexAttribute>(inherit: true).Any() ||
 1677115               member.GetCustomAttributes<CommandRegexAttribute>(inherit: true).Any() ||
 1677116               member.GetCustomAttributes<CallbackAttribute>(inherit: true).Any() ||
 1677117               HasGenericCallbackAttribute(member) ||
 1677118               member.GetCustomAttributes<ChatMemberUpdatedAttribute>(inherit: true).Any() ||
 1677119               member.GetCustomAttributes<MyChatMemberUpdatedAttribute>(inherit: true).Any();
 120    }
 121
 122    private static List<TelegramHandlerDescriptor> BuildForMethod(
 123        Type handlerType,
 124        MethodInfo method,
 125        int firstRegistrationOrder,
 126        string? moduleName,
 127        bool includeClassRouteAttributes = false)
 128    {
 358129        var callback = GetRouteAttribute<CallbackAttribute>(handlerType, method, includeClassRouteAttributes);
 358130        var callbackPayloadType = GetGenericCallbackPayloadType(handlerType, method, includeClassRouteAttributes);
 358131        var hasCallbackRoute = callback is not null || callbackPayloadType is not null;
 358132        var chatMemberUpdated = GetRouteAttribute<ChatMemberUpdatedAttribute>(handlerType, method, includeClassRouteAttr
 358133        var myChatMemberUpdated = GetRouteAttribute<MyChatMemberUpdatedAttribute>(handlerType, method, includeClassRoute
 358134        var hasChatMemberRoute = chatMemberUpdated is not null || myChatMemberUpdated is not null;
 135
 358136        var commandAttributes = GetRouteAttributes<CommandAttribute>(handlerType, method, includeClassRouteAttributes);
 358137        var message = GetRouteAttribute<MessageAttribute>(handlerType, method, includeClassRouteAttributes);
 358138        var textAttributes = GetRouteAttributes<TextAttribute>(handlerType, method, includeClassRouteAttributes);
 358139        var textTemplates = GetRouteAttributes<TextTemplateAttribute>(handlerType, method, includeClassRouteAttributes);
 358140        var commandTemplates = GetRouteAttributes<CommandTemplateAttribute>(handlerType, method, includeClassRouteAttrib
 358141        var textRegexes = GetRouteAttributes<TextRegexAttribute>(handlerType, method, includeClassRouteAttributes);
 358142        var commandRegexes = GetRouteAttributes<CommandRegexAttribute>(handlerType, method, includeClassRouteAttributes)
 358143        var filters = BuildFilters(handlerType, method);
 355144        var roleRequirements = BuildRoleRequirements(handlerType, method);
 355145        var scene = handlerType.GetCustomAttribute<SceneAttribute>(inherit: false);
 355146        var sceneStep = method.GetCustomAttribute<SceneStepAttribute>(inherit: true);
 147
 355148        var hasMessageRoute =
 355149            commandAttributes.Length > 0 ||
 355150            message is not null ||
 355151            textAttributes.Length > 0 ||
 355152            textTemplates.Length > 0 ||
 355153            commandTemplates.Length > 0 ||
 355154            textRegexes.Length > 0 ||
 355155            commandRegexes.Length > 0;
 355156        var autoAnswerCallback = BuildAutoAnswerCallback(handlerType, method, hasCallbackRoute);
 157
 355158        if (!hasCallbackRoute && !hasMessageRoute)
 159        {
 44160            if (!hasChatMemberRoute)
 161            {
 26162                if (sceneStep is not null)
 163                {
 1164                    throw CreateSignatureException(
 1165                        method,
 1166                        "SceneStepAttribute requires an explicit Telegram route attribute.");
 167                }
 168
 25169                return [];
 170            }
 171        }
 172
 329173        if (sceneStep is not null && scene is null)
 174        {
 0175            throw CreateSignatureException(
 0176                method,
 0177                "SceneStepAttribute can only be used on handler methods declared inside a SceneAttribute type.");
 178        }
 179
 329180        if (sceneStep is not null && HasStateAttributes(handlerType, method))
 181        {
 1182            throw CreateSignatureException(
 1183                method,
 1184                "SceneStepAttribute cannot be mixed with StateAttribute or StateAttribute<TStateGroup>.");
 185        }
 186
 328187        if (callback is not null && callbackPayloadType is not null)
 188        {
 0189            throw CreateSignatureException(
 0190                method,
 0191                "Raw CallbackAttribute and CallbackAttribute<TPayload> cannot be used on the same handler method.");
 192        }
 193
 328194        if (hasCallbackRoute && hasMessageRoute)
 195        {
 0196            throw CreateSignatureException(
 0197                method,
 0198                "Callback routes cannot be mixed with message, text, command, template, or regex routes.");
 199        }
 200
 328201        if (hasChatMemberRoute && (hasCallbackRoute || hasMessageRoute))
 202        {
 0203            throw CreateSignatureException(
 0204                method,
 0205                "Chat member update routes cannot be mixed with message, text, command, template, regex, or callback rou
 206        }
 207
 328208        if (!hasChatMemberRoute && HasChatMemberTransitionAttributes(handlerType, method))
 209        {
 0210            throw CreateSignatureException(
 0211                method,
 0212                "Chat member transition attributes can only be used on chat member update handlers.");
 213        }
 214
 328215        if (hasCallbackRoute &&
 352216            filters.Any(static filter => filter.Kind is not null &&
 352217                                         !TelegramFilterCompatibility.Supports(filter.Kind.Value, TelegramHandlerKind.Ca
 218        {
 4219            var invalidFilter = filters.First(filter => filter.Kind is not null &&
 4220                                                        !TelegramFilterCompatibility.Supports(filter.Kind.Value, Telegra
 2221            throw CreateSignatureException(
 2222                method,
 2223                TelegramFilterCompatibility.GetInvalidPlacementMessage(invalidFilter.Kind!.Value, TelegramHandlerKind.Ca
 224        }
 225
 326226        if (!hasCallbackRoute &&
 326227            !hasChatMemberRoute &&
 378228            filters.Any(static filter => filter.Kind is not null &&
 378229                                         !TelegramFilterCompatibility.Supports(filter.Kind.Value, TelegramHandlerKind.Me
 230        {
 0231            var invalidFilter = filters.First(filter => filter.Kind is not null &&
 0232                                                        !TelegramFilterCompatibility.Supports(filter.Kind.Value, Telegra
 0233            throw CreateSignatureException(
 0234                method,
 0235                TelegramFilterCompatibility.GetInvalidPlacementMessage(invalidFilter.Kind!.Value, TelegramHandlerKind.Me
 236        }
 237
 326238        if (hasChatMemberRoute &&
 328239            filters.Any(static filter => filter.Kind is not null &&
 328240                                         !TelegramFilterCompatibility.Supports(filter.Kind.Value, TelegramHandlerKind.Ch
 241        {
 2242            var invalidFilter = filters.First(filter => filter.Kind is not null &&
 2243                                                        !TelegramFilterCompatibility.Supports(filter.Kind.Value, Telegra
 1244            throw CreateSignatureException(
 1245                method,
 1246                TelegramFilterCompatibility.GetInvalidPlacementMessage(invalidFilter.Kind!.Value, TelegramHandlerKind.Ch
 247        }
 248
 325249        ValidateReturnType(method);
 324250        ValidateCallbackPayloadType(method, callbackPayloadType);
 251
 324252        var states = TelegramStateAttributeResolver.GetStates(handlerType, method).ToList();
 253
 324254        if (sceneStep is not null)
 255        {
 11256            states.Add(ResolveSceneStepState(handlerType, method, scene!, sceneStep));
 257        }
 258
 322259        List<RouteDefinition> routeDefinitions = hasChatMemberRoute
 322260            ? BuildChatMemberRouteDefinitions(handlerType, method, chatMemberUpdated, myChatMemberUpdated)
 322261            : hasCallbackRoute
 322262                ? [CreateCallbackRouteDefinition(callbackPayloadType)]
 322263                : BuildMessageRouteDefinitions(method, message, textAttributes, commandAttributes, textTemplates, comman
 264
 318265        var routeValues = ResolveRouteValueTypes(method, routeDefinitions);
 315266        var kind = hasChatMemberRoute
 315267            ? TelegramHandlerKind.ChatMember
 315268            : hasCallbackRoute
 315269                ? TelegramHandlerKind.Callback
 315270                : TelegramHandlerKind.Message;
 271
 315272        ValidateClassBasedHandlerCompatibility(handlerType, method, kind, callbackPayloadType);
 273
 315274        ValidateCustomFilters(method, filters, kind);
 275
 314276        var parameters = BuildParameterDescriptors(method, kind, callbackPayloadType, routeValues);
 313277        var descriptors = new List<TelegramHandlerDescriptor>(routeDefinitions.Count);
 278
 1266279        for (var index = 0; index < routeDefinitions.Count; index++)
 280        {
 320281            var definition = routeDefinitions[index];
 320282            var route = new TelegramRouteDescriptor(
 320283                definition.RouteKind,
 320284                definition.Pattern,
 320285                definition.CommandPolicy,
 320286                definition.TextFilters,
 320287                callbackPayloadType,
 320288                routeValues
 29289                    .Select(static value => value.Value)
 320290                    .ToArray(),
 320291                filters,
 320292                definition.ChatMemberTransitions,
 320293                roleRequirements);
 294
 320295            descriptors.Add(new TelegramHandlerDescriptor(
 320296                handlerType,
 320297                method,
 320298                route,
 320299                firstRegistrationOrder + index,
 320300                moduleName,
 320301                states,
 320302                parameters,
 320303                sceneStep is null ? null : scene!.Prefix,
 320304                autoAnswerCallback));
 305        }
 306
 313307        return descriptors;
 308    }
 309
 310    private static List<TelegramFilterDescriptor> BuildFilters(
 311        Type handlerType,
 312        MethodInfo method)
 313    {
 358314        var filters = new List<TelegramFilterDescriptor>();
 315
 358316        AppendFilters(filters, handlerType);
 358317        AppendFilters(filters, method);
 318
 355319        return filters;
 320    }
 321
 322    private static TelegramRoleRequirementDescriptor[] BuildRoleRequirements(
 323        Type handlerType,
 324        MethodInfo method)
 325    {
 355326        return handlerType
 355327            .GetCustomAttributes<RequireTelegramRoleAttribute>(inherit: true)
 355328            .Concat(method.GetCustomAttributes<RequireTelegramRoleAttribute>(inherit: true))
 12329            .Select(static attribute => new TelegramRoleRequirementDescriptor(attribute.AllowedStatuses))
 355330            .ToArray();
 331    }
 332
 333    private static TelegramAutoAnswerCallbackDescriptor? BuildAutoAnswerCallback(
 334        Type handlerType,
 335        MethodInfo method,
 336        bool hasCallbackRoute)
 337    {
 355338        var methodAttribute = method.GetCustomAttribute<AutoAnswerCallbackAttribute>(inherit: true);
 339
 355340        if (methodAttribute is not null && !hasCallbackRoute)
 341        {
 0342            throw new InvalidOperationException(
 0343                $"{nameof(AutoAnswerCallbackAttribute)} can be used only on callback handlers.");
 344        }
 345
 355346        if (!hasCallbackRoute)
 347        {
 285348            return null;
 349        }
 350
 70351        var attribute = methodAttribute ??
 70352            handlerType.GetCustomAttribute<AutoAnswerCallbackAttribute>(inherit: true);
 353
 70354        return attribute is null
 70355            ? null
 70356            : new TelegramAutoAnswerCallbackDescriptor(
 70357                attribute.Enabled,
 70358                NormalizeAutoAnswerText(attribute.Text),
 70359                attribute.ShowAlert);
 360    }
 361
 362    private static string? NormalizeAutoAnswerText(string? text)
 363    {
 5364        if (text is null)
 365        {
 1366            return null;
 367        }
 368
 4369        if (string.IsNullOrWhiteSpace(text))
 370        {
 0371            throw new InvalidOperationException("Auto callback answer text must not be empty.");
 372        }
 373
 4374        return text;
 375    }
 376
 377    private static void AppendFilters(
 378        List<TelegramFilterDescriptor> filters,
 379        MemberInfo member)
 380    {
 716381        var chatType = member.GetCustomAttribute<ChatTypeAttribute>(inherit: true);
 382
 716383        if (chatType is not null)
 384        {
 7385            filters.Add(new TelegramFilterDescriptor(
 7386                TelegramFilterKind.ChatType,
 7387                chatType.ChatTypes.Select(TelegramFilterFacts.MapChatType).ToArray(),
 7388                longValues: []));
 389        }
 390
 715391        var senderChatType = member.GetCustomAttribute<SenderChatTypeAttribute>(inherit: true);
 392
 715393        if (senderChatType is not null)
 394        {
 5395            filters.Add(new TelegramFilterDescriptor(
 5396                TelegramFilterKind.SenderChatType,
 5397                senderChatType.ChatTypes.Select(TelegramFilterFacts.MapChatType).ToArray(),
 5398                longValues: []));
 399        }
 400
 713401        var chatId = member.GetCustomAttribute<ChatIdAttribute>(inherit: true);
 402
 713403        if (chatId is not null)
 404        {
 4405            filters.Add(new TelegramFilterDescriptor(
 4406                TelegramFilterKind.ChatId,
 4407                stringValues: [],
 4408                chatId.ChatIds.ToArray()));
 409        }
 410
 713411        var chatUsername = member.GetCustomAttribute<ChatUsernameAttribute>(inherit: true);
 412
 713413        if (chatUsername is not null)
 414        {
 2415            filters.Add(new TelegramFilterDescriptor(
 2416                TelegramFilterKind.ChatUsername,
 2417                chatUsername.Usernames.ToArray(),
 2418                longValues: []));
 419        }
 420
 713421        var fromUser = member.GetCustomAttribute<FromUserAttribute>(inherit: true);
 422
 713423        if (fromUser is not null)
 424        {
 7425            filters.Add(new TelegramFilterDescriptor(
 7426                TelegramFilterKind.FromUser,
 7427                stringValues: [],
 7428                fromUser.UserIds.ToArray()));
 429        }
 430
 713431        AppendMarkerFilters(filters, member, TelegramMarkerFilterGroup.MessageContent);
 432
 713433        var fromBot = member.GetCustomAttribute<FromBotAttribute>(inherit: true);
 434
 713435        if (fromBot is not null)
 436        {
 3437            filters.Add(new TelegramFilterDescriptor(
 3438                TelegramFilterKind.FromBot,
 3439                stringValues: [],
 3440                fromBot.BotIds.ToArray()));
 441        }
 442
 713443        AppendMarkerFilters(filters, member, TelegramMarkerFilterGroup.SenderUser);
 713444        AppendMarkerFilters(filters, member, TelegramMarkerFilterGroup.MessageSender);
 445
 713446        var messageThreadId = member.GetCustomAttribute<MessageThreadIdAttribute>(inherit: true);
 447
 713448        if (messageThreadId is not null)
 449        {
 2450            filters.Add(new TelegramFilterDescriptor(
 2451                TelegramFilterKind.MessageThreadId,
 2452                stringValues: [],
 2453                messageThreadId.MessageThreadIds.ToArray()));
 454        }
 455
 713456        AppendMarkerFilters(filters, member, TelegramMarkerFilterGroup.SharedMetadata);
 457
 713458        var callbackDataPrefix = member.GetCustomAttribute<CallbackDataPrefixAttribute>(inherit: true);
 459
 713460        if (callbackDataPrefix is not null)
 461        {
 8462            filters.Add(new TelegramFilterDescriptor(
 8463                TelegramFilterKind.CallbackDataPrefix,
 8464                [callbackDataPrefix.Prefix],
 8465                longValues: []));
 466        }
 467
 1458468        foreach (var customFilter in GetCustomFilters(member))
 469        {
 16470            filters.Add(customFilter);
 471        }
 713472    }
 473
 474    private static void AppendMarkerFilters(
 475        List<TelegramFilterDescriptor> filters,
 476        MemberInfo member,
 477        TelegramMarkerFilterGroup group)
 478    {
 34224479        foreach (var spec in TelegramFilterFacts.GetMarkerFilters(group))
 480        {
 14260481            if (TelegramFilterFacts.HasMarkerFilter(member, spec))
 482            {
 27483                filters.Add(new TelegramFilterDescriptor(spec.Kind, stringValues: [], longValues: []));
 484            }
 485        }
 2852486    }
 487
 488    private static IEnumerable<TelegramFilterDescriptor> GetCustomFilters(MemberInfo member)
 489    {
 3264490        foreach (var attribute in member.GetCustomAttributes(inherit: true))
 491        {
 919492            var attributeType = attribute.GetType();
 493
 919494            if (attributeType.IsGenericType &&
 919495                attributeType.GetGenericTypeDefinition() == typeof(UseFilterAttribute<>))
 496            {
 15497                yield return new TelegramFilterDescriptor(attributeType.GetGenericArguments()[0]);
 15498                continue;
 499            }
 500
 904501            if (TryGetTelegramFilterAttributeFilterType(attributeType, out var filterType))
 502            {
 1503                yield return new TelegramFilterDescriptor(filterType, (Attribute)attribute);
 504            }
 505        }
 713506    }
 507
 508    private static bool TryGetTelegramFilterAttributeFilterType(
 509        Type attributeType,
 510        out Type filterType)
 511    {
 8296512        for (var current = attributeType; current is not null; current = current.BaseType)
 513        {
 3245514            if (current.IsGenericType &&
 3245515                current.GetGenericTypeDefinition() == typeof(TelegramFilterAttribute<>))
 516            {
 1517                filterType = current.GetGenericArguments()[0];
 1518                return true;
 519            }
 520        }
 521
 903522        filterType = null!;
 903523        return false;
 524    }
 525
 526    private static void ValidateCustomFilters(
 527        MethodInfo method,
 528        IReadOnlyList<TelegramFilterDescriptor> filters,
 529        TelegramHandlerKind kind)
 530    {
 736531        foreach (var filter in filters.Where(static filter => filter.CustomFilterType is not null))
 532        {
 16533            var filterType = filter.CustomFilterType!;
 534
 16535            if (filterType.IsInterface ||
 16536                filterType.IsAbstract ||
 16537                filterType.ContainsGenericParameters)
 538            {
 0539                throw CreateSignatureException(
 0540                    method,
 0541                    $"Custom Telegram filter type '{filterType.FullName}' must be a concrete closed type.");
 542            }
 543
 16544            var expectedContextType = kind switch
 16545            {
 3546                TelegramHandlerKind.Callback => typeof(CallbackQueryContext),
 0547                TelegramHandlerKind.ChatMember => typeof(ChatMemberUpdatedContext),
 13548                _ => typeof(MessageContext)
 16549            };
 550
 16551            if (filter.CustomFilterAttribute is null)
 552            {
 15553                var contextTypes = GetCustomFilterContextTypes(filterType, attributeType: null);
 554
 15555                if (contextTypes.Length == 0)
 556                {
 0557                    throw CreateSignatureException(
 0558                        method,
 0559                        $"Custom Telegram filter type '{filterType.FullName}' must implement ITelegramFilter<TContext>."
 560                }
 561
 15562                if (!SupportsContextType(contextTypes, expectedContextType))
 563                {
 1564                    throw CreateSignatureException(
 1565                        method,
 1566                        $"Custom Telegram filter type '{filterType.FullName}' is not compatible with {expectedContextTyp
 567                }
 568
 569                continue;
 570            }
 571
 1572            var attributeType = filter.CustomFilterAttribute.GetType();
 573
 1574            if (attributeType.IsGenericType)
 575            {
 0576                throw CreateSignatureException(
 0577                    method,
 0578                    $"Custom Telegram filter attribute type '{attributeType.FullName}' must be non-generic.");
 579            }
 580
 1581            var typedContextTypes = GetCustomFilterContextTypes(filterType, attributeType);
 582
 1583            if (typedContextTypes.Length == 0)
 584            {
 0585                throw CreateSignatureException(
 0586                    method,
 0587                    "Parameterized custom Telegram filter attributes require a filter type that implements ITelegramFilt
 588            }
 589
 1590            if (!SupportsContextType(typedContextTypes, expectedContextType))
 591            {
 0592                throw CreateSignatureException(
 0593                    method,
 0594                    $"Custom Telegram filter type '{filterType.FullName}' is not compatible with {expectedContextType.Na
 595            }
 596        }
 314597    }
 598
 599    private static Type[] GetCustomFilterContextTypes(
 600        Type filterType,
 601        Type? attributeType)
 602    {
 16603        var interfaces = filterType.GetInterfaces();
 604
 16605        if (attributeType is null)
 606        {
 15607            return interfaces
 15608                .Where(static type => type.IsGenericType &&
 15609                                      type.GetGenericTypeDefinition() == typeof(ITelegramFilter<>))
 15610                .Select(static type => type.GetGenericArguments()[0])
 15611                .ToArray();
 612        }
 613
 1614        return interfaces
 1615            .Where(type => type.IsGenericType &&
 1616                           type.GetGenericTypeDefinition() == typeof(ITelegramFilter<,>) &&
 1617                           type.GetGenericArguments()[1] == attributeType)
 1618            .Select(static type => type.GetGenericArguments()[0])
 1619            .ToArray();
 620    }
 621
 622    private static bool SupportsContextType(
 623        IReadOnlyCollection<Type> contextTypes,
 624        Type expectedContextType)
 625    {
 16626        return contextTypes.Contains(expectedContextType) ||
 16627               contextTypes.Contains(typeof(TelegramUpdateContext));
 628    }
 629
 630    private static string? GetModuleName(Type handlerType)
 631    {
 356632        return handlerType.GetCustomAttribute<TelegramModuleAttribute>(inherit: false)?.Name;
 633    }
 634
 635    private static bool IsClassBasedHandlerType(Type handlerType)
 636    {
 671637        return typeof(MessageHandler).IsAssignableFrom(handlerType) ||
 671638               typeof(CallbackHandler).IsAssignableFrom(handlerType) ||
 671639               typeof(ChatMemberUpdateHandler).IsAssignableFrom(handlerType);
 640    }
 641
 642    private static void ValidateClassBasedHandlerCompatibility(
 643        Type handlerType,
 644        MethodInfo method,
 645        TelegramHandlerKind kind,
 646        Type? callbackPayloadType)
 647    {
 315648        if (!IsClassBasedHandlerType(handlerType))
 649        {
 307650            return;
 651        }
 652
 8653        if (typeof(MessageHandler).IsAssignableFrom(handlerType))
 654        {
 6655            if (kind is TelegramHandlerKind.Message or TelegramHandlerKind.Command)
 656            {
 6657                return;
 658            }
 659
 0660            throw CreateSignatureException(
 0661                method,
 0662                $"Class-based handler type '{handlerType.FullName}' derives from {nameof(MessageHandler)} and can only u
 663        }
 664
 2665        if (typeof(ChatMemberUpdateHandler).IsAssignableFrom(handlerType))
 666        {
 1667            if (kind == TelegramHandlerKind.ChatMember)
 668            {
 1669                return;
 670            }
 671
 0672            throw CreateSignatureException(
 0673                method,
 0674                $"Class-based handler type '{handlerType.FullName}' derives from {nameof(ChatMemberUpdateHandler)} and c
 675        }
 676
 1677        if (typeof(CallbackHandler).IsAssignableFrom(handlerType))
 678        {
 1679            if (kind != TelegramHandlerKind.Callback)
 680            {
 0681                throw CreateSignatureException(
 0682                    method,
 0683                    $"Class-based handler type '{handlerType.FullName}' derives from {nameof(CallbackHandler)} and can o
 684            }
 685
 1686            var basePayloadType = GetCallbackHandlerPayloadType(handlerType);
 687
 1688            if (basePayloadType is null && callbackPayloadType is null)
 689            {
 0690                return;
 691            }
 692
 1693            if (basePayloadType is not null &&
 1694                callbackPayloadType is not null &&
 1695                basePayloadType == callbackPayloadType)
 696            {
 1697                return;
 698            }
 699
 0700            throw CreateSignatureException(
 0701                method,
 0702                $"Class-based typed callback handler '{handlerType.FullName}' must use matching CallbackAttribute<TPaylo
 703        }
 0704    }
 705
 706    private static Type? GetCallbackHandlerPayloadType(Type handlerType)
 707    {
 4708        for (var current = handlerType; current is not null; current = current.BaseType)
 709        {
 2710            if (current.IsGenericType &&
 2711                current.GetGenericTypeDefinition() == typeof(CallbackHandler<>))
 712            {
 1713                return current.GetGenericArguments()[0];
 714            }
 715        }
 716
 0717        return null;
 718    }
 719
 720    private static TAttribute? GetRouteAttribute<TAttribute>(
 721        Type handlerType,
 722        MethodInfo method,
 723        bool includeClassRouteAttributes)
 724        where TAttribute : Attribute
 725    {
 1432726        var attributes = GetRouteAttributes<TAttribute>(handlerType, method, includeClassRouteAttributes);
 1432727        return attributes.Length > 0 ? attributes[0] : null;
 728    }
 729
 730    private static TAttribute[] GetRouteAttributes<TAttribute>(
 731        Type handlerType,
 732        MethodInfo method,
 733        bool includeClassRouteAttributes)
 734        where TAttribute : Attribute
 735    {
 3580736        var methodAttributes = method.GetCustomAttributes<TAttribute>(inherit: true);
 737
 3580738        return includeClassRouteAttributes
 3580739            ? handlerType.GetCustomAttributes<TAttribute>(inherit: true)
 3580740                .Concat(methodAttributes)
 3580741                .ToArray()
 3580742            : methodAttributes.ToArray();
 743    }
 744
 745    private static List<RouteDefinition> BuildMessageRouteDefinitions(
 746        MethodInfo method,
 747        MessageAttribute? message,
 748        IReadOnlyList<TextAttribute> textAttributes,
 749        IReadOnlyList<CommandAttribute> commandAttributes,
 750        IReadOnlyList<TextTemplateAttribute> textTemplates,
 751        IReadOnlyList<CommandTemplateAttribute> commandTemplates,
 752        IReadOnlyList<TextRegexAttribute> textRegexes,
 753        IReadOnlyList<CommandRegexAttribute> commandRegexes)
 754    {
 237755        var definitions = new List<RouteDefinition>();
 756
 599757        foreach (var command in commandAttributes)
 758        {
 63759            var policy = CreateCommandPolicy(command.Prefixes, command.AllowSpaceAfterPrefix, command.IgnoreCase, comman
 62760            definitions.Add(new RouteDefinition(
 62761                TelegramRouteKind.CommandExact,
 62762                NormalizeCommand(method, command.Command),
 62763                policy,
 62764                TextFilters: [],
 62765                RouteValues: CreateEmptyRouteValues(),
 62766                ChatMemberTransitions: []));
 767        }
 768
 499769        foreach (var template in commandTemplates)
 770        {
 14771            var policy = CreateCommandPolicy(template.Prefixes, template.AllowSpaceAfterPrefix, template.IgnoreCase, tem
 14772            definitions.Add(new RouteDefinition(
 14773                TelegramRouteKind.CommandTemplate,
 14774                NormalizeCommandPattern(method, template.Template, policy),
 14775                policy,
 14776                TextFilters: [],
 14777                TelegramTemplateRouteParser.GetRouteValues(template.Template)
 14778                    .ToDictionary(
 11779                        static value => value.Name,
 11780                        static value => new RouteValueDefinition(value.ValueType, value.IsOptional),
 14781                        StringComparer.Ordinal),
 14782                ChatMemberTransitions: []));
 783        }
 784
 476785        foreach (var regex in commandRegexes)
 786        {
 3787            var policy = CreateCommandPolicy(regex.Prefixes, regex.AllowSpaceAfterPrefix, regex.IgnoreCase, regex.Prefix
 3788            var routeValues = GetRegexRouteValueNames(method, regex.Pattern, regex.IgnoreCase)
 3789                .ToDictionary(
 2790                    static name => name,
 2791                    static _ => new RouteValueDefinition(ValueType: null, IsOptional: false),
 3792                    StringComparer.Ordinal);
 3793            definitions.Add(new RouteDefinition(
 3794                TelegramRouteKind.CommandRegex,
 3795                regex.Pattern,
 3796                policy,
 3797                TextFilters: [],
 3798                routeValues,
 3799                ChatMemberTransitions: []));
 800        }
 801
 235802        if (message is not null)
 803        {
 135804            definitions.Add(new RouteDefinition(
 135805                TelegramRouteKind.MessageAny,
 135806                Pattern: null,
 135807                CommandPolicy: null,
 135808                textAttributes
 3809                    .Select(static attribute => new TelegramTextFilter(attribute.Value, attribute.Mode, attribute.Ignore
 135810                    .ToArray(),
 135811                RouteValues: CreateEmptyRouteValues(),
 135812                ChatMemberTransitions: []));
 813        }
 814        else
 815        {
 216816            foreach (var text in textAttributes)
 817            {
 8818                definitions.Add(new RouteDefinition(
 8819                    TelegramRouteKind.TextExact,
 8820                    text.Value,
 8821                    CommandPolicy: null,
 8822                    [new TelegramTextFilter(text.Value, text.Mode, text.IgnoreCase)],
 8823                    RouteValues: CreateEmptyRouteValues(),
 8824                    ChatMemberTransitions: []));
 825            }
 826        }
 827
 502828        foreach (var template in textTemplates)
 829        {
 17830            definitions.Add(new RouteDefinition(
 17831                TelegramRouteKind.TextTemplate,
 17832                template.Template,
 17833                CommandPolicy: new TelegramCommandPolicy(["/"], allowSpaceAfterPrefix: false, template.IgnoreCase),
 17834                TextFilters: [],
 17835                TelegramTemplateRouteParser.GetRouteValues(template.Template)
 17836                    .ToDictionary(
 15837                        static value => value.Name,
 15838                        static value => new RouteValueDefinition(value.ValueType, value.IsOptional),
 17839                        StringComparer.Ordinal),
 17840                ChatMemberTransitions: []));
 841        }
 842
 474843        foreach (var regex in textRegexes)
 844        {
 4845            var routeValues = GetRegexRouteValueNames(method, regex.Pattern, regex.IgnoreCase)
 4846                .ToDictionary(
 4847                    static name => name,
 4848                    static _ => new RouteValueDefinition(ValueType: null, IsOptional: false),
 4849                    StringComparer.Ordinal);
 4850            definitions.Add(new RouteDefinition(
 4851                TelegramRouteKind.TextRegex,
 4852                regex.Pattern,
 4853                CommandPolicy: new TelegramCommandPolicy(["/"], allowSpaceAfterPrefix: false, regex.IgnoreCase),
 4854                TextFilters: [],
 4855                routeValues,
 4856                ChatMemberTransitions: []));
 857        }
 858
 233859        return definitions;
 860    }
 861
 862    private static RouteDefinition CreateCallbackRouteDefinition(Type? callbackPayloadType)
 863    {
 68864        return new RouteDefinition(
 68865            TelegramRouteKind.Callback,
 68866            Pattern: null,
 68867            CommandPolicy: null,
 68868            TextFilters: [],
 68869            RouteValues: CreateEmptyRouteValues(),
 68870            ChatMemberTransitions: []);
 871    }
 872
 873    private static List<RouteDefinition> BuildChatMemberRouteDefinitions(
 874        Type handlerType,
 875        MethodInfo method,
 876        ChatMemberUpdatedAttribute? chatMemberUpdated,
 877        MyChatMemberUpdatedAttribute? myChatMemberUpdated)
 878    {
 17879        var transitions = BuildChatMemberTransitions(handlerType, method);
 17880        var definitions = new List<RouteDefinition>(capacity: 2);
 881
 17882        if (chatMemberUpdated is not null)
 883        {
 15884            definitions.Add(new RouteDefinition(
 15885                TelegramRouteKind.ChatMemberUpdated,
 15886                Pattern: null,
 15887                CommandPolicy: null,
 15888                TextFilters: [],
 15889                RouteValues: CreateEmptyRouteValues(),
 15890                transitions));
 891        }
 892
 17893        if (myChatMemberUpdated is not null)
 894        {
 2895            definitions.Add(new RouteDefinition(
 2896                TelegramRouteKind.MyChatMemberUpdated,
 2897                Pattern: null,
 2898                CommandPolicy: null,
 2899                TextFilters: [],
 2900                RouteValues: CreateEmptyRouteValues(),
 2901                transitions));
 902        }
 903
 17904        return definitions;
 905    }
 906
 907    private static TelegramChatMemberTransitionDescriptor[] BuildChatMemberTransitions(
 908        Type handlerType,
 909        MethodInfo method)
 910    {
 17911        return GetChatMemberTransitionAttributes(handlerType, method)
 7912            .Select(static attribute => TelegramMemberTransitionFacts.Map(attribute.Transition))
 17913            .Concat(GetChatMemberChangedAttributes(handlerType, method)
 1914                .Select(static attribute => new TelegramChatMemberTransitionDescriptor(
 1915                    attribute.OldStatus,
 1916                    attribute.NewStatus)))
 17917            .ToArray();
 918    }
 919
 920    private static IEnumerable<ChatMemberTransitionAttribute> GetChatMemberTransitionAttributes(
 921        Type handlerType,
 922        MethodInfo method)
 923    {
 327924        return handlerType
 327925            .GetCustomAttributes<ChatMemberTransitionAttribute>(inherit: true)
 327926            .Concat(method.GetCustomAttributes<ChatMemberTransitionAttribute>(inherit: true));
 927    }
 928
 929    private static IEnumerable<ChatMemberChangedAttribute> GetChatMemberChangedAttributes(
 930        Type handlerType,
 931        MethodInfo method)
 932    {
 327933        return handlerType
 327934            .GetCustomAttributes<ChatMemberChangedAttribute>(inherit: true)
 327935            .Concat(method.GetCustomAttributes<ChatMemberChangedAttribute>(inherit: true));
 936    }
 937
 938    private static bool HasChatMemberTransitionAttributes(
 939        Type handlerType,
 940        MethodInfo method)
 941    {
 310942        return GetChatMemberTransitionAttributes(handlerType, method).Any() ||
 310943               GetChatMemberChangedAttributes(handlerType, method).Any();
 944    }
 945
 946    private static bool HasStateAttributes(
 947        Type handlerType,
 948        MethodInfo method)
 949    {
 12950        return handlerType.GetCustomAttributes<StateAttribute>(inherit: true).Any() ||
 12951               method.GetCustomAttributes<StateAttribute>(inherit: true).Any() ||
 12952               HasGenericStateAttributes(handlerType) ||
 12953               HasGenericStateAttributes(method);
 954    }
 955
 956    private static bool HasGenericStateAttributes(MemberInfo member)
 957    {
 22958        return member
 22959            .GetCustomAttributes(inherit: true)
 56960            .Select(static attribute => attribute.GetType())
 22961            .Any(static type =>
 78962                type.IsGenericType &&
 78963                type.GetGenericTypeDefinition() == typeof(StateAttribute<>));
 964    }
 965
 966    private static string ResolveSceneStepState(
 967        Type handlerType,
 968        MethodInfo method,
 969        SceneAttribute scene,
 970        SceneStepAttribute step)
 971    {
 11972        var property = handlerType.GetProperty(
 11973            step.StateName,
 11974            BindingFlags.Public | BindingFlags.Static);
 975
 11976        if (property is null || property.PropertyType != typeof(State))
 977        {
 1978            throw CreateSignatureException(
 1979                method,
 1980                $"SceneStepAttribute references missing scene state '{handlerType.FullName}.{step.StateName}'.");
 981        }
 982
 10983        if (property.GetValue(null) is not State state || string.IsNullOrWhiteSpace(state.Id))
 984        {
 0985            throw CreateSignatureException(
 0986                method,
 0987                $"Scene state '{handlerType.FullName}.{step.StateName}' must return a non-empty State value.");
 988        }
 989
 10990        var expectedStateId = $"{scene.Prefix}:{GetSceneStateSegment(property)}";
 991
 10992        if (!string.Equals(state.Id, expectedStateId, StringComparison.Ordinal))
 993        {
 1994            throw CreateSignatureException(
 1995                method,
 1996                $"Scene state '{handlerType.FullName}.{step.StateName}' must return canonical state id '{expectedStateId
 997        }
 998
 9999        return state.Id;
 1000    }
 1001
 1002    private static string GetSceneStateSegment(PropertyInfo property)
 1003    {
 101004        var attribute = property.GetCustomAttribute<StateValueAttribute>(inherit: false);
 1005
 101006        return attribute is not null && !string.IsNullOrWhiteSpace(attribute.Value)
 101007            ? attribute.Value
 101008            : ToCamelCase(property.Name);
 1009    }
 1010
 1011    private static string ToCamelCase(string value)
 1012    {
 91013        return value.Length == 0
 91014            ? value
 91015            : char.ToLowerInvariant(value[0]) + value[1..];
 1016    }
 1017
 1018    private static Dictionary<string, TelegramRouteValueDescriptor> ResolveRouteValueTypes(
 1019        MethodInfo method,
 1020        IReadOnlyList<RouteDefinition> routeDefinitions)
 1021    {
 3181022        var routeValueDefinitions = routeDefinitions
 3251023            .Select(static definition => definition.RouteValues)
 3251024            .Where(static values => values.Count > 0)
 3181025            .ToArray();
 1026
 3181027        if (routeValueDefinitions.Length == 0)
 1028        {
 2861029            return new Dictionary<string, TelegramRouteValueDescriptor>(StringComparer.Ordinal);
 1030        }
 1031
 321032        var firstNames = routeValueDefinitions[0].Keys.Order(StringComparer.Ordinal).ToArray();
 1033
 641034        foreach (var values in routeValueDefinitions.Skip(1))
 1035        {
 01036            var names = values.Keys.Order(StringComparer.Ordinal).ToArray();
 1037
 01038            if (!firstNames.SequenceEqual(names, StringComparer.Ordinal))
 1039            {
 01040                throw CreateSignatureException(
 01041                    method,
 01042                    "Multiple route attributes on one handler method must expose the same route value names.");
 1043            }
 1044        }
 1045
 641046        if (routeDefinitions.Any(static definition => definition.RouteValues.Count == 0))
 1047        {
 01048            throw CreateSignatureException(
 01049                method,
 01050                "A handler method with route value parameters cannot also have routes that do not provide route values."
 1051        }
 1052
 321053        var parameters = method.GetParameters();
 321054        var routeValues = new Dictionary<string, TelegramRouteValueDescriptor>(StringComparer.Ordinal);
 1055
 1251056        foreach (var name in firstNames)
 1057        {
 951058            var parameter = parameters.FirstOrDefault(parameter => string.Equals(parameter.Name, name, StringComparison.
 1059
 321060            if (parameter is null)
 1061            {
 11062                throw CreateSignatureException(
 11063                    method,
 11064                    $"Route value '{name}' must have a matching handler parameter with the same name.");
 1065            }
 1066
 311067            if (!TryGetRouteValueParameterShape(parameter, out var parameterValueType, out var parameterIsNullable))
 1068            {
 01069                throw CreateSignatureException(
 01070                    method,
 01071                    $"Route value parameter '{name}' must be string, string?, int, int?, long, or long?.");
 1072            }
 1073
 1221074            foreach (var values in routeValueDefinitions)
 1075            {
 311076                var definition = values[name];
 1077
 311078                if (definition.IsOptional != parameterIsNullable)
 1079                {
 21080                    var expectedNullability = definition.IsOptional ? "nullable" : "non-nullable";
 21081                    throw CreateSignatureException(
 21082                        method,
 21083                        $"Route value parameter '{name}' must be {expectedNullability} for the selected route template."
 1084                }
 1085
 291086                if (definition.ValueType is { } expectedType && expectedType != parameterValueType)
 1087                {
 01088                    throw CreateSignatureException(
 01089                        method,
 01090                        $"Route value parameter '{name}' must be {expectedType.Name} for the selected route template.");
 1091                }
 1092            }
 1093
 291094            routeValues.Add(name, new TelegramRouteValueDescriptor(name, parameterValueType, parameterIsNullable));
 1095        }
 1096
 291097        return routeValues;
 1098    }
 1099
 1100    private static List<TelegramHandlerParameterDescriptor> BuildParameterDescriptors(
 1101        MethodInfo method,
 1102        TelegramHandlerKind kind,
 1103        Type? callbackPayloadType,
 1104        Dictionary<string, TelegramRouteValueDescriptor> routeValues)
 1105    {
 3141106        var expectedContextType = kind switch
 3141107        {
 671108            TelegramHandlerKind.Callback => typeof(CallbackQueryContext),
 171109            TelegramHandlerKind.ChatMember => typeof(ChatMemberUpdatedContext),
 2301110            _ => typeof(MessageContext)
 3141111        };
 1112
 3141113        var parameters = method.GetParameters();
 3141114        var contextParameters = parameters
 6491115            .Where(parameter => parameter.ParameterType == typeof(MessageContext) ||
 6491116                                parameter.ParameterType == typeof(CallbackQueryContext) ||
 6491117                                parameter.ParameterType == typeof(ChatMemberUpdatedContext) ||
 6491118                                parameter.ParameterType == typeof(TelegramUpdateContext))
 3141119            .ToArray();
 1120
 3141121        if (contextParameters.Length != 1 || contextParameters[0].ParameterType != expectedContextType)
 1122        {
 01123            throw CreateSignatureException(
 01124                method,
 01125                $"A {FormatHandlerKind(kind)} handler must declare exactly one {expectedContextType.Name} parameter.");
 1126        }
 1127
 9631128        if (parameters.Count(static parameter => parameter.ParameterType == typeof(CancellationToken)) > 1)
 1129        {
 01130            throw CreateSignatureException(method, "A handler method can declare at most one CancellationToken parameter
 1131        }
 1132
 3141133        var descriptors = new List<TelegramHandlerParameterDescriptor>(parameters.Length);
 1134
 19251135        foreach (var parameter in parameters)
 1136        {
 6491137            if (parameter.ParameterType == expectedContextType)
 1138            {
 3141139                descriptors.Add(new TelegramHandlerParameterDescriptor(parameter, TelegramHandlerParameterKind.Context))
 3141140                continue;
 1141            }
 1142
 3351143            if (parameter.ParameterType == typeof(CancellationToken))
 1144            {
 171145                descriptors.Add(new TelegramHandlerParameterDescriptor(parameter, TelegramHandlerParameterKind.Cancellat
 171146                continue;
 1147            }
 1148
 3181149            if (!string.IsNullOrWhiteSpace(parameter.Name) && routeValues.ContainsKey(parameter.Name))
 1150            {
 291151                descriptors.Add(new TelegramHandlerParameterDescriptor(parameter, TelegramHandlerParameterKind.RouteValu
 291152                continue;
 1153            }
 1154
 2891155            if (kind == TelegramHandlerKind.Callback && callbackPayloadType == parameter.ParameterType)
 1156            {
 181157                descriptors.Add(new TelegramHandlerParameterDescriptor(parameter, TelegramHandlerParameterKind.CallbackP
 181158                continue;
 1159            }
 1160
 2711161            if (kind == TelegramHandlerKind.Callback &&
 2711162                callbackPayloadType is null &&
 2711163                IsCallbackDataPayloadType(parameter.ParameterType))
 1164            {
 11165                throw CreateSignatureException(
 11166                    method,
 11167                    $"Raw {nameof(CallbackAttribute)} handlers do not bind typed callback payloads. Use {nameof(Callback
 1168            }
 1169
 2701170            descriptors.Add(new TelegramHandlerParameterDescriptor(parameter, TelegramHandlerParameterKind.Service));
 1171        }
 1172
 3131173        if (kind == TelegramHandlerKind.Callback && callbackPayloadType is not null)
 1174        {
 711175            var payloadParameterCount = parameters.Count(parameter => parameter.ParameterType == callbackPayloadType);
 1176
 181177            if (payloadParameterCount != 1)
 1178            {
 01179                throw CreateSignatureException(
 01180                    method,
 01181                    $"{nameof(CallbackAttribute)}<{callbackPayloadType.Name}> handlers must declare exactly one {callbac
 1182            }
 1183        }
 1184
 3131185        return descriptors;
 1186    }
 1187
 1188    private static TelegramCommandPolicy CreateCommandPolicy(
 1189        IReadOnlyList<string> prefixes,
 1190        bool allowSpaceAfterPrefix,
 1191        bool ignoreCase,
 1192        CommandPrefixMode prefixMode)
 1193    {
 801194        return new TelegramCommandPolicy(prefixes, allowSpaceAfterPrefix, ignoreCase, prefixMode);
 1195    }
 1196
 1197    private static Dictionary<string, RouteValueDefinition> CreateEmptyRouteValues()
 1198    {
 2901199        return new Dictionary<string, RouteValueDefinition>(StringComparer.Ordinal);
 1200    }
 1201
 1202    private static string[] GetRegexRouteValueNames(
 1203        MethodInfo method,
 1204        string pattern,
 1205        bool ignoreCase)
 1206    {
 1207        Regex regex;
 1208
 1209        try
 1210        {
 71211            regex = new Regex(pattern, TelegramTemplateRouteParser.GetRegexOptions(ignoreCase));
 71212        }
 01213        catch (ArgumentException exception)
 1214        {
 01215            throw CreateSignatureException(method, $"Regex route pattern is invalid: {exception.Message}");
 1216        }
 1217
 71218        return regex
 71219            .GetGroupNames()
 131220            .Where(static name => !int.TryParse(name, out _))
 71221            .Distinct(StringComparer.Ordinal)
 71222            .ToArray();
 1223    }
 1224
 1225    private static Type? GetGenericCallbackPayloadType(
 1226        Type handlerType,
 1227        MethodInfo method,
 1228        bool includeClassRouteAttributes)
 1229    {
 3581230        var attributes = method
 3581231            .GetCustomAttributes(inherit: true)
 8191232            .Select(static attribute => attribute.GetType())
 3581233            .Where(static type =>
 11771234                type.IsGenericType &&
 11771235                type.GetGenericTypeDefinition() == typeof(CallbackAttribute<>));
 1236
 3581237        if (includeClassRouteAttributes)
 1238        {
 81239            attributes = handlerType
 81240                .GetCustomAttributes(inherit: true)
 141241                .Select(static attribute => attribute.GetType())
 81242                .Where(static type =>
 141243                    type.IsGenericType &&
 141244                    type.GetGenericTypeDefinition() == typeof(CallbackAttribute<>))
 81245                .Concat(attributes);
 1246        }
 1247
 3581248        var payloadTypes = attributes
 181249            .Select(static type => type.GetGenericArguments()[0])
 3581250            .Distinct()
 3581251            .ToArray();
 1252
 3581253        if (payloadTypes.Length > 1)
 1254        {
 01255            throw CreateSignatureException(
 01256                method,
 01257                "CallbackAttribute<TPayload> metadata on one handler must resolve to exactly one payload type.");
 1258        }
 1259
 3581260        return payloadTypes.FirstOrDefault();
 1261    }
 1262
 1263    private static bool HasGenericCallbackAttribute(MemberInfo member)
 1264    {
 16741265        return member
 16741266            .GetCustomAttributes(inherit: true)
 7461267            .Select(static attribute => attribute.GetType())
 16741268            .Any(static type =>
 24201269                type.IsGenericType &&
 24201270                type.GetGenericTypeDefinition() == typeof(CallbackAttribute<>));
 1271    }
 1272
 1273    private static void ValidateCallbackPayloadType(MethodInfo method, Type? callbackPayloadType)
 1274    {
 3241275        if (callbackPayloadType is null)
 1276        {
 3061277            return;
 1278        }
 1279
 181280        if (callbackPayloadType.IsInterface ||
 181281            callbackPayloadType.IsAbstract ||
 181282            callbackPayloadType.ContainsGenericParameters)
 1283        {
 01284            throw CreateSignatureException(
 01285                method,
 01286                $"{nameof(CallbackAttribute)} payload type '{callbackPayloadType.FullName}' must be a concrete closed ty
 1287        }
 181288    }
 1289
 1290    private static bool IsCallbackDataPayloadType(Type type)
 1291    {
 461292        return type.GetCustomAttribute<CallbackDataAttribute>(inherit: false) is not null;
 1293    }
 1294
 1295    private static void ValidateReturnType(MethodInfo method)
 1296    {
 3251297        if (method.ReturnType != typeof(Task) && method.ReturnType != typeof(ValueTask))
 1298        {
 11299            throw CreateSignatureException(
 11300                method,
 11301                "A Telegram handler method must return Task or ValueTask.");
 1302        }
 3241303    }
 1304
 1305    private static string NormalizeCommand(MethodInfo method, string command)
 1306    {
 621307        var normalized = command.Trim();
 1308
 621309        if (normalized.StartsWith('/') ||
 621310            normalized.Contains('@', StringComparison.Ordinal) ||
 621311            normalized.Any(char.IsWhiteSpace))
 1312        {
 01313            throw CreateSignatureException(
 01314                method,
 01315                "CommandAttribute value must be a command name without '/', bot username, or whitespace.");
 1316        }
 1317
 621318        return normalized;
 1319    }
 1320
 1321    private static string FormatHandlerKind(TelegramHandlerKind kind)
 1322    {
 01323        return kind switch
 01324        {
 01325            TelegramHandlerKind.Command => "command",
 01326            TelegramHandlerKind.Message => "message",
 01327            TelegramHandlerKind.Callback => "callback",
 01328            TelegramHandlerKind.ChatMember => "chat member update",
 01329            _ => kind.ToString()
 01330        };
 1331    }
 1332
 1333    private static string NormalizeCommandPattern(
 1334        MethodInfo method,
 1335        string pattern,
 1336        TelegramCommandPolicy policy)
 1337    {
 141338        var normalized = pattern.Trim();
 1339
 281340        if (policy.Prefixes.Any(prefix => normalized.StartsWith(prefix, StringComparison.Ordinal)))
 1341        {
 11342            throw CreateSignatureException(
 11343                method,
 11344                "Command route template value must not include a command prefix.");
 1345        }
 1346
 131347        return normalized;
 1348    }
 1349
 1350    private static bool TryGetRouteValueParameterShape(
 1351        ParameterInfo parameter,
 1352        out Type valueType,
 1353        out bool isNullable)
 1354    {
 311355        var parameterType = parameter.ParameterType;
 311356        var underlyingType = Nullable.GetUnderlyingType(parameterType);
 1357
 311358        if (underlyingType is not null)
 1359        {
 51360            valueType = underlyingType;
 51361            isNullable = true;
 51362            return valueType == typeof(int) || valueType == typeof(long);
 1363        }
 1364
 261365        if (parameterType == typeof(string))
 1366        {
 51367            valueType = typeof(string);
 51368            isNullable = NullabilityInfoContext.Create(parameter).ReadState == NullabilityState.Nullable;
 51369            return true;
 1370        }
 1371
 211372        valueType = parameterType;
 211373        isNullable = false;
 1374
 211375        return valueType == typeof(int) || valueType == typeof(long);
 1376    }
 1377
 1378    private static InvalidOperationException CreateSignatureException(MethodInfo method, string reason)
 1379    {
 141380        return new InvalidOperationException(
 141381            $"Invalid Telegram handler signature '{method.DeclaringType?.FullName}.{method.Name}': {reason}");
 1382    }
 1383
 1384    private sealed record RouteDefinition(
 1385        TelegramRouteKind RouteKind,
 1386        string? Pattern,
 1387        TelegramCommandPolicy? CommandPolicy,
 1388        IReadOnlyList<TelegramTextFilter> TextFilters,
 1389        IReadOnlyDictionary<string, RouteValueDefinition> RouteValues,
 1390        IReadOnlyList<TelegramChatMemberTransitionDescriptor> ChatMemberTransitions);
 1391
 1392    private readonly record struct RouteValueDefinition(
 1393        Type? ValueType,
 1394        bool IsOptional);
 1395}

Methods/Properties

.cctor()
Build(System.Type,System.Int32)
GetCandidateMethods(System.Type,System.Boolean)
GetDeclaredCandidateMethods(System.Type)
HasInheritedHandlerMethods(System.Type)
HasRouteAttributes(System.Reflection.MemberInfo)
BuildForMethod(System.Type,System.Reflection.MethodInfo,System.Int32,System.String,System.Boolean)
BuildFilters(System.Type,System.Reflection.MethodInfo)
BuildRoleRequirements(System.Type,System.Reflection.MethodInfo)
BuildAutoAnswerCallback(System.Type,System.Reflection.MethodInfo,System.Boolean)
NormalizeAutoAnswerText(System.String)
AppendFilters(System.Collections.Generic.List`1<TeleFlow.Telegram.Internal.Handlers.TelegramFilterDescriptor>,System.Reflection.MemberInfo)
AppendMarkerFilters(System.Collections.Generic.List`1<TeleFlow.Telegram.Internal.Handlers.TelegramFilterDescriptor>,System.Reflection.MemberInfo,TeleFlow.Telegram.Internal.Handlers.TelegramMarkerFilterGroup)
GetCustomFilters()
TryGetTelegramFilterAttributeFilterType(System.Type,System.Type&)
ValidateCustomFilters(System.Reflection.MethodInfo,System.Collections.Generic.IReadOnlyList`1<TeleFlow.Telegram.Internal.Handlers.TelegramFilterDescriptor>,TeleFlow.Telegram.Internal.Handlers.TelegramHandlerKind)
GetCustomFilterContextTypes(System.Type,System.Type)
SupportsContextType(System.Collections.Generic.IReadOnlyCollection`1<System.Type>,System.Type)
GetModuleName(System.Type)
IsClassBasedHandlerType(System.Type)
ValidateClassBasedHandlerCompatibility(System.Type,System.Reflection.MethodInfo,TeleFlow.Telegram.Internal.Handlers.TelegramHandlerKind,System.Type)
GetCallbackHandlerPayloadType(System.Type)
GetRouteAttribute(System.Type,System.Reflection.MethodInfo,System.Boolean)
GetRouteAttributes(System.Type,System.Reflection.MethodInfo,System.Boolean)
BuildMessageRouteDefinitions(System.Reflection.MethodInfo,TeleFlow.Annotations.MessageAttribute,System.Collections.Generic.IReadOnlyList`1<TeleFlow.Annotations.TextAttribute>,System.Collections.Generic.IReadOnlyList`1<TeleFlow.Annotations.CommandAttribute>,System.Collections.Generic.IReadOnlyList`1<TeleFlow.Annotations.TextTemplateAttribute>,System.Collections.Generic.IReadOnlyList`1<TeleFlow.Annotations.CommandTemplateAttribute>,System.Collections.Generic.IReadOnlyList`1<TeleFlow.Annotations.TextRegexAttribute>,System.Collections.Generic.IReadOnlyList`1<TeleFlow.Annotations.CommandRegexAttribute>)
CreateCallbackRouteDefinition(System.Type)
BuildChatMemberRouteDefinitions(System.Type,System.Reflection.MethodInfo,TeleFlow.Annotations.ChatMemberUpdatedAttribute,TeleFlow.Annotations.MyChatMemberUpdatedAttribute)
BuildChatMemberTransitions(System.Type,System.Reflection.MethodInfo)
GetChatMemberTransitionAttributes(System.Type,System.Reflection.MethodInfo)
GetChatMemberChangedAttributes(System.Type,System.Reflection.MethodInfo)
HasChatMemberTransitionAttributes(System.Type,System.Reflection.MethodInfo)
HasStateAttributes(System.Type,System.Reflection.MethodInfo)
HasGenericStateAttributes(System.Reflection.MemberInfo)
ResolveSceneStepState(System.Type,System.Reflection.MethodInfo,TeleFlow.Annotations.SceneAttribute,TeleFlow.Annotations.SceneStepAttribute)
GetSceneStateSegment(System.Reflection.PropertyInfo)
ToCamelCase(System.String)
ResolveRouteValueTypes(System.Reflection.MethodInfo,System.Collections.Generic.IReadOnlyList`1<TeleFlow.Telegram.Internal.Handlers.TelegramHandlerDescriptorBuilder/RouteDefinition>)
BuildParameterDescriptors(System.Reflection.MethodInfo,TeleFlow.Telegram.Internal.Handlers.TelegramHandlerKind,System.Type,System.Collections.Generic.Dictionary`2<System.String,TeleFlow.Telegram.Internal.Handlers.TelegramRouteValueDescriptor>)
CreateCommandPolicy(System.Collections.Generic.IReadOnlyList`1<System.String>,System.Boolean,System.Boolean,TeleFlow.Annotations.CommandPrefixMode)
CreateEmptyRouteValues()
GetRegexRouteValueNames(System.Reflection.MethodInfo,System.String,System.Boolean)
GetGenericCallbackPayloadType(System.Type,System.Reflection.MethodInfo,System.Boolean)
HasGenericCallbackAttribute(System.Reflection.MemberInfo)
ValidateCallbackPayloadType(System.Reflection.MethodInfo,System.Type)
IsCallbackDataPayloadType(System.Type)
ValidateReturnType(System.Reflection.MethodInfo)
NormalizeCommand(System.Reflection.MethodInfo,System.String)
FormatHandlerKind(TeleFlow.Telegram.Internal.Handlers.TelegramHandlerKind)
NormalizeCommandPattern(System.Reflection.MethodInfo,System.String,TeleFlow.Telegram.Internal.Handlers.TelegramCommandPolicy)
TryGetRouteValueParameterShape(System.Reflection.ParameterInfo,System.Type&,System.Boolean&)
CreateSignatureException(System.Reflection.MethodInfo,System.String)