diff --git a/Telegrator/Annotations/CommandArgumentAttributes.cs b/Telegrator/Annotations/CommandArgumentAttributes.cs index a38f797..ceac359 100644 --- a/Telegrator/Annotations/CommandArgumentAttributes.cs +++ b/Telegrator/Annotations/CommandArgumentAttributes.cs @@ -3,22 +3,53 @@ using Telegrator.Filters; namespace Telegrator.Annotations { + /// + /// Attribute for filtering messages where a command argument starts with the specified content. + /// + /// The content that the command argument should start with. + /// The string comparison type to use for the check. + /// The index of the argument to check (0-based). public class ArgumentStartsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : MessageFilterAttribute(new ArgumentStartsWithFilter(content, comparison, index)) { } + /// + /// Attribute for filtering messages where a command argument ends with the specified content. + /// + /// The content that the command argument should end with. + /// The string comparison type to use for the check. + /// The index of the argument to check (0-based). public class ArgumentEndsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : MessageFilterAttribute(new ArgumentEndsWithFilter(content, comparison, index)) { } + /// + /// Attribute for filtering messages where a command argument contains the specified content. + /// + /// The content that the command argument should contain. + /// The string comparison type to use for the check. + /// The index of the argument to check (0-based). public class ArgumentContainsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : MessageFilterAttribute(new ArgumentContainsFilter(content, comparison, index)) { } + /// + /// Attribute for filtering messages where a command argument equals the specified content. + /// + /// The content that the command argument should equal. + /// The string comparison type to use for the check. + /// The index of the argument to check (0-based). public class ArgumentEqualsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : MessageFilterAttribute(new ArgumentEqualsFilter(content, comparison, index)) { } + /// + /// Attribute for filtering messages where a command argument matches a regular expression pattern. + /// + /// The regular expression pattern to match against the command argument. + /// The regex options to use for the pattern matching. + /// The timeout for the regex match operation. + /// The index of the argument to check (0-based). public class ArgumentRegexAttribute(string pattern, RegexOptions options = RegexOptions.None, TimeSpan matchTimeout = default, int index = 0) : MessageFilterAttribute(new ArgumentRegexFilter(pattern, options, matchTimeout, index)) { } diff --git a/Telegrator/Filters/CommandArgumentFilter.cs b/Telegrator/Filters/CommandArgumentFilter.cs index 1274e6a..13ef094 100644 --- a/Telegrator/Filters/CommandArgumentFilter.cs +++ b/Telegrator/Filters/CommandArgumentFilter.cs @@ -5,10 +5,23 @@ using Telegrator.Handlers; namespace Telegrator.Filters { + /// + /// Abstract base class for filters that operate on command arguments. + /// Provides functionality to extract and validate command arguments from message text. + /// + /// The index of the argument to filter (0-based). public abstract class CommandArgumentFilterBase(int index) : Filter { + /// + /// Gets the chosen argument at the specified index. + /// protected string Target { get; private set; } = null!; + /// + /// Determines whether the filter can pass by extracting the command argument and validating it. + /// + /// The filter execution context containing the message. + /// True if the argument exists and passes validation; otherwise, false. public override bool CanPass(FilterExecutionContext context) { CommandHandlerAttribute attr = context.CompletedFilters.Get(0); @@ -21,57 +34,151 @@ namespace Telegrator.Filters return CanPassNext(context); } + /// + /// Determines whether the filter can pass for the given context. + /// + /// The filter execution context. + /// True if the filter passes; otherwise, false. protected abstract bool CanPassNext(FilterExecutionContext context); } + /// + /// Filter that checks if a command argument starts with a specified content. + /// + /// The content to check if the argument starts with. + /// The string comparison type to use for the check. + /// The index of the argument to check (0-based). public class ArgumentStartsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : CommandArgumentFilterBase(index) { + /// + /// The content to check if the argument starts with. + /// protected readonly string Content = content; + + /// + /// The string comparison type to use for the check. + /// protected readonly StringComparison Comparison = comparison; + /// + /// Checks if the command argument starts with the specified content using the configured comparison. + /// + /// The filter execution context (unused). + /// True if the argument starts with the specified content; otherwise, false. protected override bool CanPassNext(FilterExecutionContext _) => Target.StartsWith(Content, Comparison); } + /// + /// Filter that checks if a command argument ends with a specified content. + /// + /// The content to check if the argument ends with. + /// The string comparison type to use for the check. + /// The index of the argument to check (0-based). public class ArgumentEndsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : CommandArgumentFilterBase(index) { + /// + /// The content to check if the argument ends with. + /// protected readonly string Content = content; + + /// + /// The string comparison type to use for the check. + /// protected readonly StringComparison Comparison = comparison; + /// + /// Checks if the command argument ends with the specified content using the configured comparison. + /// + /// The filter execution context (unused). + /// True if the argument ends with the specified content; otherwise, false. protected override bool CanPassNext(FilterExecutionContext _) => Target.EndsWith(Content, Comparison); } + /// + /// Filter that checks if a command argument contains a specified content. + /// + /// The content to check if the argument contains. + /// The string comparison type to use for the check. + /// The index of the argument to check (0-based). public class ArgumentContainsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : CommandArgumentFilterBase(index) { + /// + /// The content to check if the argument contains. + /// protected readonly string Content = content; + + /// + /// The string comparison type to use for the check. + /// protected readonly StringComparison Comparison = comparison; + /// + /// Checks if the command argument contains the specified content using the configured comparison. + /// + /// The filter execution context (unused). + /// True if the argument contains the specified content; otherwise, false. protected override bool CanPassNext(FilterExecutionContext _) => Target.IndexOf(Content, Comparison) >= 0; } + /// + /// Filter that checks if a command argument equals a specified content. + /// + /// The content to check if the argument equals. + /// The string comparison type to use for the check. + /// The index of the argument to check (0-based). public class ArgumentEqualsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : CommandArgumentFilterBase(index) { + /// + /// The content to check if the argument equals. + /// protected readonly string Content = content; + + /// + /// The string comparison type to use for the check. + /// protected readonly StringComparison Comparison = comparison; + /// + /// Checks if the command argument equals the specified content using the configured comparison. + /// + /// The filter execution context (unused). + /// True if the argument equals the specified content; otherwise, false. protected override bool CanPassNext(FilterExecutionContext _) => Target.Equals(Content, Comparison); } - public class ArgumentRegexFilter : CommandArgumentFilterBase + /// + /// Filter that checks if a command argument matches a regular expression pattern. + /// + /// The regular expression to match against the argument. + /// The index of the argument to check (0-based). + public class ArgumentRegexFilter(Regex regex, int index = 0) : CommandArgumentFilterBase(index) { - private readonly Regex _regex; + private readonly Regex _regex = regex; + /// + /// Gets the match found by the regex. + /// public Match Match { get; private set; } = null!; - public ArgumentRegexFilter(Regex regex, int index = 0) - : base(index) => _regex = regex; - + /// + /// Initializes a new instance of with a regex pattern. + /// + /// The regular expression pattern to match. + /// The regex options to use. + /// The timeout for the regex match operation. + /// The index of the argument to check (0-based). public ArgumentRegexFilter(string pattern, RegexOptions options = RegexOptions.None, TimeSpan matchTimeout = default, int index = 0) - : base(index) => _regex = new Regex(pattern, options, matchTimeout); + : this(new Regex(pattern, options, matchTimeout), index) { } + /// + /// Checks if the command argument matches the regular expression pattern. + /// + /// The filter execution context. + /// True if the argument matches the regex pattern; otherwise, false. protected override bool CanPassNext(FilterExecutionContext context) { Match = _regex.Match(Target); diff --git a/Telegrator/Filters/MessageFilters.cs b/Telegrator/Filters/MessageFilters.cs index fd7cb3e..5fcffd7 100644 --- a/Telegrator/Filters/MessageFilters.cs +++ b/Telegrator/Filters/MessageFilters.cs @@ -107,7 +107,7 @@ namespace Telegrator.Filters /// public class DiceThrowedFilter : MessageFilterBase { - private readonly DiceType? Dice; + private readonly DiceType Dice; private readonly int Value; /// @@ -132,7 +132,7 @@ namespace Telegrator.Filters if (Target.Dice == null) return false; - if (Dice != null && Target.Dice.Emoji != GetEmojyForDiceType(Dice)) + if (Target.Dice.Emoji != GetEmojyForDiceType(Dice)) return false; return Target.Dice.Value == Value; diff --git a/Telegrator/Handlers/AbstractHandlerContainer.cs b/Telegrator/Handlers/AbstractHandlerContainer.cs index 6709a3b..88ed8dc 100644 --- a/Telegrator/Handlers/AbstractHandlerContainer.cs +++ b/Telegrator/Handlers/AbstractHandlerContainer.cs @@ -33,6 +33,10 @@ namespace Telegrator.Handlers /// public IAwaitingProvider AwaitingProvider { get; } + /// + /// Initializes new instance of + /// + /// public AbstractHandlerContainer(DescribedHandlerInfo handlerInfo) { ActualUpdate = handlerInfo.HandlingUpdate.GetActualUpdateObject(); @@ -43,6 +47,15 @@ namespace Telegrator.Handlers AwaitingProvider = handlerInfo.AwaitingProvider; } + /// + /// Initializes new instance of + /// + /// + /// + /// + /// + /// + /// public AbstractHandlerContainer(TUpdate actualUpdate, Update handlingUpdate, ITelegramBotClient client, Dictionary extraData, CompletedFiltersList filters, IAwaitingProvider awaitingProvider) { ActualUpdate = actualUpdate; @@ -53,6 +66,11 @@ namespace Telegrator.Handlers AwaitingProvider = awaitingProvider; } + /// + /// Creates new container of specific update type from thos contatiner + /// + /// + /// public AbstractHandlerContainer CreateChild() where QUpdate : class { return new AbstractHandlerContainer( @@ -61,6 +79,12 @@ namespace Telegrator.Handlers CompletedFilters, AwaitingProvider); } + /// + /// Creates new container of specific update type from existing container + /// + /// + /// + /// public static AbstractHandlerContainer From(IAbstractHandlerContainer other) where QUpdate : class { return new AbstractHandlerContainer( diff --git a/Telegrator/Handlers/CommandHandler.cs b/Telegrator/Handlers/CommandHandler.cs index 627e49c..4d130b0 100644 --- a/Telegrator/Handlers/CommandHandler.cs +++ b/Telegrator/Handlers/CommandHandler.cs @@ -16,6 +16,9 @@ namespace Telegrator.Handlers /// public string ReceivedCommand { get; private set; } = null!; + /// + /// Message text splited by space characters + /// public string[]? Arguments { get; internal set; } = null; /// @@ -32,7 +35,10 @@ namespace Telegrator.Handlers if (commandEntity.Type != MessageEntityType.BotCommand) return false; - ReceivedCommand = message.Text.Substring(commandEntity.Offset + 1, commandEntity.Length - 1); + if (commandEntity.Offset != 0) + return false; + + ReceivedCommand = message.Text.Substring(1, commandEntity.Length - 1); if (ReceivedCommand.Contains('@')) { string[] split = ReceivedCommand.Split('@'); diff --git a/Telegrator/Handlers/InlineQueryHandler.cs b/Telegrator/Handlers/InlineQueryHandler.cs index 62038b1..7a12d7b 100644 --- a/Telegrator/Handlers/InlineQueryHandler.cs +++ b/Telegrator/Handlers/InlineQueryHandler.cs @@ -7,21 +7,41 @@ using Telegrator.Handlers.Components; namespace Telegrator.Handlers { + /// + /// Attribute that marks a handler to process inline queries. + /// public class InlineQueryHandlerAttribute(int importance = 0) : UpdateHandlerAttribute(UpdateType.InlineQuery, importance) { + /// public override bool CanPass(FilterExecutionContext context) => context.Input.InlineQuery is { } | context.Input.ChosenInlineResult is { }; } - + + /// + /// Abstract base class for handlers that process inline queries. + /// public abstract class InlineQueryHandler() : AbstractUpdateHandler(UpdateType.InlineQuery) { + /// + /// Handler container for the current update. + /// protected IAbstractHandlerContainer QueryContainer { get; private set; } = null!; + /// + /// Handler container for the current update. + /// protected IAbstractHandlerContainer ChosenContainer { get; private set; } = null!; + /// + /// Incoming update of type . + /// protected InlineQuery InputQuery { get; private set; } = null!; + /// + /// Incoming update of type . + /// protected ChosenInlineResult InputChosen { get; private set; } = null!; + /// public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { switch (container.HandlingUpdate.Type) @@ -30,14 +50,14 @@ namespace Telegrator.Handlers { QueryContainer = AbstractHandlerContainer.From(container); InputQuery = QueryContainer.ActualUpdate; - return await Requested(QueryContainer, cancellation); + return await Requested(QueryContainer, cancellation).ConfigureAwait(false); } case UpdateType.ChosenInlineResult: { ChosenContainer = AbstractHandlerContainer.From(container); InputChosen = ChosenContainer.ActualUpdate; - return await Chosen(ChosenContainer, cancellation); + return await Chosen(ChosenContainer, cancellation).ConfigureAwait(false); } default: @@ -45,10 +65,32 @@ namespace Telegrator.Handlers } } + /// + /// Executes handler logic if received update is + /// + /// + /// + /// public abstract Task Requested(IAbstractHandlerContainer container, CancellationToken cancellation); + /// + /// Executes handler logic if received update is + /// + /// + /// + /// public abstract Task Chosen(IAbstractHandlerContainer container, CancellationToken cancellation); + /// + /// Answers inline query + /// + /// + /// + /// + /// + /// + /// + /// protected async Task Answer( IEnumerable results, int? cacheTime = null, diff --git a/Telegrator/Polling/UpdateHandlersPool.cs b/Telegrator/Polling/UpdateHandlersPool.cs index 21a1451..5b3dd49 100644 --- a/Telegrator/Polling/UpdateHandlersPool.cs +++ b/Telegrator/Polling/UpdateHandlersPool.cs @@ -72,7 +72,7 @@ namespace Telegrator.Polling if (ExecutingHandlersSemaphore != null) { - await ExecutingHandlersSemaphore.WaitAsync(); + await ExecutingHandlersSemaphore.WaitAsync().ConfigureAwait(false); } try @@ -82,7 +82,11 @@ namespace Telegrator.Polling using (UpdateHandlerBase instance = handlerInfo.HandlerInstance) { - lastResult = await instance.Execute(handlerInfo); + Task task = instance.Execute(handlerInfo); + HandlerEnqueued?.Invoke(handlerInfo); + + await task.ConfigureAwait(false); + lastResult = task.Result; ExecutingHandlersSemaphore?.Release(1); } diff --git a/Telegrator/TypesExtensions.cs b/Telegrator/TypesExtensions.cs index b577f7c..c190d24 100644 --- a/Telegrator/TypesExtensions.cs +++ b/Telegrator/TypesExtensions.cs @@ -42,6 +42,12 @@ namespace Telegrator return message.Text.Substring(entity.Offset, entity.Length); } + /// + /// Checkes if sent contains command. Automatically cuts bot name from it + /// + /// + /// + /// public static bool IsCommand(this Message message, out string? command) { command = null; @@ -52,7 +58,10 @@ namespace Telegrator if (commandEntity.Type != MessageEntityType.BotCommand) return false; - command = message.Text.Substring(commandEntity.Offset + 1, commandEntity.Length - 1); + if (commandEntity.Offset != 0) + return false; + + command = message.Text.Substring(1, commandEntity.Length - 1); if (command.Contains('@')) { string[] split = command.Split('@'); @@ -62,9 +71,17 @@ namespace Telegrator return true; } + /// + /// Split message text into arguments, ignoring command instance. Splits by space character + /// + /// + /// + /// + /// + /// public static string[] SplitArgs(this Message message) { - if (!message.IsCommand(out string? command)) + if (!message.IsCommand(out _)) throw new InvalidDataException("Message does not contain a command"); if (message is not { Text.Length: > 0 }) @@ -76,10 +93,16 @@ namespace Telegrator return message.Text.Split([' '], StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray(); } + /// + /// Tries to split message text into arguments, ignoring command instance. Splits by space character. Exception-free version of + /// + /// + /// + /// public static bool TrySplitArgs(this Message message, out string[]? args) { args = null; - if (!message.IsCommand(out string? command)) + if (!message.IsCommand(out _)) return false; if (message is not { Text.Length: > 0 }) @@ -150,21 +173,37 @@ namespace Telegrator /// public static class AbstractHandlerContainerExtensions { + /// + /// Changes bot's reaction to message + /// + /// + /// + /// + /// + /// public static async Task React( this IAbstractHandlerContainer container, - IEnumerable reactions, + ReactionType reaction, bool isBig = false, CancellationToken cancellationToken = default) => await container.Client.SetMessageReaction( container.ActualUpdate.Chat, container.ActualUpdate.Id, - reactions, isBig, cancellationToken); + [reaction], isBig, cancellationToken); + /// + /// Changes bot's reaction to message + /// + /// + /// + /// + /// + /// public static async Task React( this IAbstractHandlerContainer container, + IEnumerable reactions, bool isBig = false, - CancellationToken cancellationToken = default, - params IEnumerable reactions) + CancellationToken cancellationToken = default) => await container.Client.SetMessageReaction( container.ActualUpdate.Chat, container.ActualUpdate.Id, @@ -365,6 +404,17 @@ namespace Telegrator cacheTime: cacheTime, cancellationToken: cancellationToken); + /// + /// Answers inline query + /// + /// + /// + /// + /// + /// + /// + /// + /// public static async Task AnswerInlineQuery( this IAbstractHandlerContainer container, IEnumerable results,