5 Commits

Author SHA1 Message Date
Rikitav 5f9c640930 * Added missing summaries 2026-03-09 04:38:03 +04:00
Rikitav 0e445dd586 Added README 2026-03-09 04:19:36 +04:00
Rikitav da090627ff * Added Redis state storage implementation 2026-03-09 03:49:03 +04:00
Rikitav 88bd12aadd * Readme and docs updated 2026-03-09 03:31:45 +04:00
Rikitav 162d4a1d05 * StateKeeper system rework
* StateKeepers are deleted
* Added IStateMachine and IStateStorage
* Added IStateStorage as provider to containers and handlers
* Added default IStateStorage implementation
* Added default StateMachine
* minor bug fixes
2026-03-09 03:22:23 +04:00
64 changed files with 1296 additions and 2125 deletions
+2 -3
View File
@@ -91,13 +91,12 @@ bot.Handlers.AddHandler<StartCommandHandler>();
using Telegrator.Handlers; using Telegrator.Handlers;
using Telegrator.Annotations; using Telegrator.Annotations;
[CommandHandler, CommandAlias("first"), NumericState(SpecialState.NoState)] [CommandHandler, CommandAlias("first"), State<SetupWizard>(null)]
public class StateKeepFirst : CommandHandler public class StateKeepFirst : CommandHandler
{ {
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation) public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{ {
container.CreateNumericState(); StateStorage.GetStateMachine<SetupWizard>().BysenderId().Advance();
container.ForwardNumericState();
await Reply("first state moved (1)", cancellationToken: cancellation); await Reply("first state moved (1)", cancellationToken: cancellation);
return Result.Ok(); return Result.Ok();
} }
+1
View File
@@ -1,5 +1,6 @@
<Solution> <Solution>
<Project Path="dev/Telegrator.RoslynGenerators/Telegrator.RoslynGenerators.csproj" /> <Project Path="dev/Telegrator.RoslynGenerators/Telegrator.RoslynGenerators.csproj" />
<Project Path="src/Telegartor.RedisStateStorage/Telegartor.RedisStateStorage.csproj" />
<Project Path="src/Telegrator.Analyzers/Telegrator.Analyzers.csproj" /> <Project Path="src/Telegrator.Analyzers/Telegrator.Analyzers.csproj" />
<Project Path="src/Telegrator.Hosting.Web/Telegrator.Hosting.Web.csproj" /> <Project Path="src/Telegrator.Hosting.Web/Telegrator.Hosting.Web.csproj" />
<Project Path="src/Telegrator.Hosting/Telegrator.Hosting.csproj" /> <Project Path="src/Telegrator.Hosting/Telegrator.Hosting.csproj" />
@@ -5,10 +5,6 @@ using System.Collections.Immutable;
using System.Text; using System.Text;
using Telegrator.RoslynGenerators.RoslynExtensions; using Telegrator.RoslynGenerators.RoslynExtensions;
#if DEBUG
using System.Diagnostics;
#endif
namespace Telegrator.RoslynGenerators; namespace Telegrator.RoslynGenerators;
[Generator(LanguageNames.CSharp)] [Generator(LanguageNames.CSharp)]
@@ -72,6 +68,9 @@ public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator
if (className == "FilterAnnotation") if (className == "FilterAnnotation")
continue; continue;
if (className == "StateAttribute")
continue;
MethodDeclarationSyntax? targeter = classDeclaration.Members.OfType<MethodDeclarationSyntax>().SingleOrDefault(IsTargeterMethod); MethodDeclarationSyntax? targeter = classDeclaration.Members.OfType<MethodDeclarationSyntax>().SingleOrDefault(IsTargeterMethod);
if (targeter != null) if (targeter != null)
{ {
@@ -101,7 +100,13 @@ public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator
continue; continue;
usings.UnionAdd(classDeclaration.FindAncestor<CompilationUnitSyntax>().Usings, UsingEqualityComparer); usings.UnionAdd(classDeclaration.FindAncestor<CompilationUnitSyntax>().Usings, UsingEqualityComparer);
MethodDeclarationSyntax targeter = FindTargetterMethod(targetters, classDeclaration); MethodDeclarationSyntax? targeter = FindTargetterMethod(targetters, classDeclaration);
if (targeter == null)
{
debugExport.AppendLine("Targetter not found");
continue;
}
if (classDeclaration.ParameterList != null && classDeclaration.BaseList != null) if (classDeclaration.ParameterList != null && classDeclaration.BaseList != null)
{ {
@@ -140,16 +145,15 @@ public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator
{ {
ClassDeclarationSyntax extensionsClass = SyntaxFactory.ClassDeclaration("HandlerBuilderExtensions") ClassDeclarationSyntax extensionsClass = SyntaxFactory.ClassDeclaration("HandlerBuilderExtensions")
.WithModifiers(Modifiers(SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword, SyntaxKind.PartialKeyword)) .WithModifiers(Modifiers(SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword, SyntaxKind.PartialKeyword))
.AddMembers([.. targetters.Values, .. extensions]) .AddMembers([.. targetters.Values, .. extensions]);
.DecorateType(1);
NamespaceDeclarationSyntax namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName("Telegrator")) NamespaceDeclarationSyntax namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName("Telegrator"))
.WithMembers([extensionsClass]) .WithMembers([extensionsClass]);
.Decorate();
CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit() CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit()
.WithUsings([.. usings]) .WithUsings([.. usings])
.WithMembers([namespaceDeclaration]); .WithMembers([namespaceDeclaration])
.NormalizeWhitespace();
context.AddSource("GeneratedHandlerBuilderExtensions.cs", compilationUnit.ToFullString()); context.AddSource("GeneratedHandlerBuilderExtensions.cs", compilationUnit.ToFullString());
} }
@@ -175,7 +179,7 @@ public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator
if (targetterMethod.ExpressionBody != null) if (targetterMethod.ExpressionBody != null)
method = method.WithExpressionBody(targetterMethod.ExpressionBody).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); method = method.WithExpressionBody(targetterMethod.ExpressionBody).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
return method.DecorateMember(2); return method;
} }
private static MethodDeclarationSyntax GeneratedExtensionsMethod(ClassDeclarationSyntax classDeclaration, ParameterListSyntax methodParameters, ArgumentListSyntax invokerArguments, MethodDeclarationSyntax targetterMethod) private static MethodDeclarationSyntax GeneratedExtensionsMethod(ClassDeclarationSyntax classDeclaration, ParameterListSyntax methodParameters, ArgumentListSyntax invokerArguments, MethodDeclarationSyntax targetterMethod)
@@ -204,11 +208,10 @@ public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator
MethodDeclarationSyntax method = SyntaxFactory.MethodDeclaration(returnType, identifier) MethodDeclarationSyntax method = SyntaxFactory.MethodDeclaration(returnType, identifier)
.WithParameterList(parameters) .WithParameterList(parameters)
.WithBody(body.DecorateBlock(2)) .WithBody(body)
.WithTypeParameterList(typeParameters) .WithTypeParameterList(typeParameters)
.WithModifiers(Modifiers(SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword)) .WithModifiers(Modifiers(SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword))
.WithConstraintClauses([typeParameterConstraint]) .WithConstraintClauses([typeParameterConstraint])
.DecorateMember(2)
.WithLeadingTrivia(xmlDoc); .WithLeadingTrivia(xmlDoc);
return method; return method;
@@ -230,7 +233,7 @@ public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator
private static IEnumerable<ConstructorDeclarationSyntax> GetConstructors(ClassDeclarationSyntax classDeclaration) private static IEnumerable<ConstructorDeclarationSyntax> GetConstructors(ClassDeclarationSyntax classDeclaration)
=> classDeclaration.Members.OfType<ConstructorDeclarationSyntax>().Where(ctor => ctor.Modifiers.HasModifiers("public")); => classDeclaration.Members.OfType<ConstructorDeclarationSyntax>().Where(ctor => ctor.Modifiers.HasModifiers("public"));
private static MethodDeclarationSyntax FindTargetterMethod(Dictionary<string, MethodDeclarationSyntax> targeters, ClassDeclarationSyntax classDeclaration) private static MethodDeclarationSyntax? FindTargetterMethod(Dictionary<string, MethodDeclarationSyntax> targeters, ClassDeclarationSyntax classDeclaration)
{ {
if (targeters.TryGetValue(classDeclaration.Identifier.ValueText, out MethodDeclarationSyntax targeter)) if (targeters.TryGetValue(classDeclaration.Identifier.ValueText, out MethodDeclarationSyntax targeter))
return targeter; return targeter;
@@ -238,7 +241,7 @@ public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator
if (classDeclaration.BaseList != null && targeters.TryGetValue(classDeclaration.BaseList.Types.ElementAt(0).Type.ToString(), out targeter)) if (classDeclaration.BaseList != null && targeters.TryGetValue(classDeclaration.BaseList.Types.ElementAt(0).Type.ToString(), out targeter))
return targeter; return targeter;
throw new TargteterNotFoundException(); return null;
} }
private static SyntaxTriviaList BuildExtensionXmlDocTrivia(ClassDeclarationSyntax classDeclaration, ParameterListSyntax methodParameters) private static SyntaxTriviaList BuildExtensionXmlDocTrivia(ClassDeclarationSyntax classDeclaration, ParameterListSyntax methodParameters)
@@ -1,38 +0,0 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Telegrator.RoslynGenerators.RoslynExtensions
{
public static class MemberDeclarationSyntaxExtensions
{
private static SyntaxTrivia TabulationTrivia => SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, "\t");
private static SyntaxTrivia WhitespaceTrivia => SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " ");
private static SyntaxTrivia NewLineTrivia => SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, "\n");
public static SyntaxTokenList Decorate(this SyntaxTokenList tokens)
=> new SyntaxTokenList(tokens.Select(token => token.WithoutTrivia().WithTrailingTrivia(WhitespaceTrivia)).ToArray());
public static BlockSyntax DecorateBlock(this BlockSyntax block, int times) => block
.WithStatements([.. block.Statements.Select(statement => statement.DecorateStatememnt(times + 1))])
.WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithLeadingTrivia(WhitespaceTrivia).WithTrailingTrivia(NewLineTrivia))
.WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken).WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia));
public static T DecorateStatememnt<T>(this T statememnt, int times) where T : StatementSyntax => statememnt
.WithoutTrivia().WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia);
public static T DecorateMember<T>(this T typeDeclaration, int times) where T : MemberDeclarationSyntax => typeDeclaration
.WithoutTrivia().WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia);
public static NamespaceDeclarationSyntax Decorate(this NamespaceDeclarationSyntax namespaceDeclaration) => namespaceDeclaration
.WithName(namespaceDeclaration.Name.WithoutTrivia().WithLeadingTrivia(WhitespaceTrivia))
.WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithLeadingTrivia(NewLineTrivia).WithTrailingTrivia(NewLineTrivia))
.WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken));
public static T DecorateType<T>(this T typeDeclaration, int times = 1) where T : TypeDeclarationSyntax => (T)typeDeclaration
.WithoutTrivia().WithLeadingTrivia(TabulationTrivia.Repeat(times))
.WithIdentifier(typeDeclaration.Identifier.WithoutTrivia().WithLeadingTrivia(WhitespaceTrivia).WithTrailingTrivia(NewLineTrivia))
.WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia))
.WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken).WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia));
}
}
+29 -35
View File
@@ -47,7 +47,7 @@ This guide will walk you through the core concepts and advanced features of **Te
**Telegrator** is distributed as a NuGet package. You can install it using the .NET CLI, the NuGet Package Manager Console, or by managing NuGet packages in Visual Studio. **Telegrator** is distributed as a NuGet package. You can install it using the .NET CLI, the NuGet Package Manager Console, or by managing NuGet packages in Visual Studio.
### Prerequisites ### Prerequisites
- .NET >= 5.0 `or` .NET Core >= 2.0 `or` Framework >= 4.6.1 (.NET Standard 2.0 compatible) - .NET >= 5.0 `or` .NET Core >= 2.0 `or` Framework >= 4.6.1 (.NET Standard 2.1 compatible)
- A Telegram Bot Token from [@BotFather](https://t.me/BotFather). - A Telegram Bot Token from [@BotFather](https://t.me/BotFather).
### .NET CLI ### .NET CLI
@@ -61,7 +61,7 @@ Install-Package Telegrator
``` ```
### Hosting Integrations ### Hosting Integrations
- .NET Core >= 8.0 - .NET Core >= 10.0
- `Telegrator.Hosting`: For console/background services - `Telegrator.Hosting`: For console/background services
- `Telegrator.Hosting.Web`: For ASP.NET Core/Webhook - `Telegrator.Hosting.Web`: For ASP.NET Core/Webhook
@@ -325,7 +325,7 @@ builder.Handlers.AddMethod<CallbackQuery>(Option1Handler);
```csharp ```csharp
public enum UserState public enum UserState
{ {
Start = SpecialState.NoState, Start,
WaitingForName, WaitingForName,
WaitingForAge WaitingForAge
} }
@@ -333,39 +333,40 @@ public enum UserState
// Start conversation // Start conversation
[CommandHandler] [CommandHandler]
[CommandAlias("register")] [CommandAlias("register")]
[EnumState<UserState>(UserState.Start)] [State<UserState>(UserState.Start)]
private static async Task<Result> StartRegistration(IHandlerContainer<Message> container, CancellationToken cancellationToken) private static async Task<Result> StartRegistration(IHandlerContainer<Message> container, CancellationToken cancellationToken)
{ {
container.ForwardEnumState<UserState>(); StateStorage.GetStateMachine<UserState>().BySenderId().Advance();
await container.Reply("Please enter your name:", cancellationToken: cancellationToken); await container.Reply("Please enter your name:", cancellationToken: cancellationToken);
return Ok; return Ok;
} }
// Handle name input // Handle name input
[MessageHandler] [MessageHandler]
[EnumState<UserState>(UserState.WaitingForName)] [State<UserState>(UserState.WaitingForName)]
private static async Task<Result> HandleName(IHandlerContainer<Message> container, CancellationToken cancellationToken) private static async Task<Result> HandleName(IHandlerContainer<Message> container, CancellationToken cancellationToken)
{ {
var name = container.Input.Text; var name = container.Input.Text;
container.ForwardEnumState<UserState>(); StateStorage.GetStateMachine<UserState>().BySenderId().Advance();
await container.Reply($"Hello {name}! Please enter your age:", cancellationToken: cancellationToken); await container.Reply($"Hello {name}! Please enter your age:", cancellationToken: cancellationToken);
return Ok; return Ok;
} }
// Handle age input // Handle age input
[MessageHandler] [MessageHandler]
[EnumState<UserState>(UserState.WaitingForAge)] [State<UserState>(UserState.WaitingForAge)]
private static async Task<Result> HandleAge(IHandlerContainer<Message> container, CancellationToken cancellationToken) private static async Task<Result> HandleAge(IHandlerContainer<Message> container, CancellationToken cancellationToken)
{ {
if (int.TryParse(container.Input.Text, out int age)) if (int.TryParse(container.Input.Text, out int age))
{ {
container.DeleteEnumState<UserState>(); StateStorage.GetStateMachine<UserState>().BySenderId().Reset();
await container.Reply($"Registration complete! Name: {name}, Age: {age}", cancellationToken: cancellationToken); await container.Reply($"Registration complete! Name: {name}, Age: {age}", cancellationToken: cancellationToken);
} }
else else
{ {
await container.Reply("Please enter a valid age (number):", cancellationToken: cancellationToken); await container.Reply("Please enter a valid age (number):", cancellationToken: cancellationToken);
} }
return Ok; return Ok;
} }
@@ -497,50 +498,43 @@ public class RestrictedHandler : MessageHandler
### 3.3. State Management ### 3.3. State Management
Telegrator provides built-in state management for multi-step conversations (wizards, forms, quizzes) without a database. Telegrator provides built-in state management for multi-step conversations (wizards, forms, quizzes) with or without a database.
> [!NOTE] > [!NOTE]
> Each type of `StateKeeper`'s (EnumStateKeeper, NumericStateKeeper) is shared between **EVERY** handler in project. > Each type of `StateKeeper`'s keys and states are shared between **EVERY** handler in project.
**Types of State:**
- **NumericState**: Integer-based steps
- **StringState**: Named steps
- **EnumState**: Enum-based scenarios
**How to Use:** **How to Use:**
1. Define your state (enum/int/string) 1. Define your state (enum/int/string)
2. Use a state filter attribute on your handler: 2. Use a state filter attribute on your handler:
- `[EnumState<MyEnum>(MyEnum.Step1)]` - `[State<MyEnum>(MyEnum.Step1)]`
- `[NumericState(1)]`
- `[StringState("waiting_input")]`
3. Change state inside the handler using extension methods: 3. Change state inside the handler using extension methods:
- `container.ForwardEnumState<MyEnum>()` - `StateStorage.GetStateMachine<MyEnum>().BySenderId().Current()`
- `container.ForwardNumericState()` - `StateStorage.GetStateMachine<MyEnum>().BySenderId().Advance()`
- `container.ForwardStringState()` - `StateStorage.GetStateMachine<MyEnum>().BySenderId().Retreat()`
- `container.DeleteEnumState<MyEnum>()` - `StateStorage.GetStateMachine<MyEnum>().BySenderId().Reset()`
**Example:** **Example:**
```csharp ```csharp
public enum QuizState public enum QuizState
{ {
Start = SpecialState.NoState, Q1, Q2 Start, Q1, Q2
} }
[CommandHandler] [CommandHandler]
[CommandAlias("quiz")] [CommandAlias("quiz")]
[EnumState<QuizState>(QuizState.Start)] [State<QuizState>(QuizState.Start)]
public class StartQuizHandler : CommandHandler public class StartQuizHandler : CommandHandler
{ {
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation) public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{ {
container.ForwardEnumState<QuizState>(); StateStorage.GetStateMachine<QuizState>().BySenderId().Advance();
await Reply("Quiz started! Question 1: What is the capital of France?"); await Reply("Quiz started! Question 1: What is the capital of France?");
return Ok; return Ok;
} }
} }
[MessageHandler] [MessageHandler]
[EnumState<QuizState>(QuizState.Q1)] [State<QuizState>(QuizState.Q1)]
public class Q1Handler : MessageHandler public class Q1Handler : MessageHandler
{ {
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation) public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
@@ -550,7 +544,7 @@ public class Q1Handler : MessageHandler
else else
await Reply("Incorrect. The answer is Paris."); await Reply("Incorrect. The answer is Paris.");
container.ForwardEnumState<QuizState>(); StateStorage.GetStateMachine<QuizState>().BySenderId().Advance();
await Reply("Question 2: What is 2 + 2?"); await Reply("Question 2: What is 2 + 2?");
return Ok; return Ok;
} }
@@ -559,8 +553,8 @@ public class Q1Handler : MessageHandler
> **How is it working?** > **How is it working?**
> 1. **Enum State Definition**: `QuizState` enum defines the conversation flow with `Start = SpecialState.NoState` indicating no initial state. > 1. **Enum State Definition**: `QuizState` enum defines the conversation flow with `Start = SpecialState.NoState` indicating no initial state.
> 2. **State Filter**: `[EnumState<QuizState>(QuizState.Start)]` ensures the handler only runs when the user is in the "Start" state. > 2. **State Filter**: `[State<QuizState>(QuizState.Start)]` ensures the handler only runs when the user is in the "Start" state.
> 3. **State Transition**: `container.ForwardEnumState<QuizState>()` moves the user to the next state (Q1). > 3. **State Transition**: `StateStorage.GetStateMachine<QuizState>().BySenderId().Advance()` moves the user to the next state (Q1).
> 4. **Next Handler**: The `Q1Handler` will only run when the user is in state `QuizState.Q1`. > 4. **Next Handler**: The `Q1Handler` will only run when the user is in state `QuizState.Q1`.
> 5. **State Management**: Each handler manages its own state transition, creating a clear conversation flow. > 5. **State Management**: Each handler manages its own state transition, creating a clear conversation flow.
@@ -1425,28 +1419,28 @@ public enum UserState
// Start conversation // Start conversation
[CommandHandler] [CommandHandler]
[CommandAlias("register")] [CommandAlias("register")]
[EnumState<UserState>(UserState.Start)] [State<UserState>(UserState.Start)]
private static async Task<Result> StartRegistration(IHandlerContainer<Message> container, CancellationToken cancellationToken) private static async Task<Result> StartRegistration(IHandlerContainer<Message> container, CancellationToken cancellationToken)
{ {
container.ForwardEnumState<UserState>(); StateStorage.GetStateMachine<UserState>().BySenderId().Advance();
await container.Reply("Please enter your name:", cancellationToken: cancellationToken); await container.Reply("Please enter your name:", cancellationToken: cancellationToken);
return Ok; return Ok;
} }
// Handle name input // Handle name input
[MessageHandler] [MessageHandler]
[EnumState<UserState>(UserState.WaitingForName)] [State<UserState>(UserState.WaitingForName)]
private static async Task<Result> HandleName(IHandlerContainer<Message> container, CancellationToken cancellationToken) private static async Task<Result> HandleName(IHandlerContainer<Message> container, CancellationToken cancellationToken)
{ {
var name = container.Input.Text; var name = container.Input.Text;
container.ForwardEnumState<UserState>(); StateStorage.GetStateMachine<UserState>().BySenderId().Advance();
await container.Reply($"Hello {name}! Please enter your age:", cancellationToken: cancellationToken); await container.Reply($"Hello {name}! Please enter your age:", cancellationToken: cancellationToken);
return Ok; return Ok;
} }
// Handle age input // Handle age input
[MessageHandler] [MessageHandler]
[EnumState<UserState>(UserState.WaitingForAge)] [State<UserState>(UserState.WaitingForAge)]
private static async Task<Result> HandleAge(IHandlerContainer<Message> container, CancellationToken cancellationToken) private static async Task<Result> HandleAge(IHandlerContainer<Message> container, CancellationToken cancellationToken)
{ {
if (int.TryParse(container.Input.Text, out int age)) if (int.TryParse(container.Input.Text, out int age))
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Telegartor.RedisStateStorage</name>
</assembly>
<members>
<member name="T:Telegrator.States.RedisStateStorage">
<summary>
Provides a Redis-based implementation of the <see cref="T:Telegrator.Core.States.IStateStorage"/> interface.
Serializes state objects to JSON format before storing them in the Redis database.
</summary>
</member>
<member name="M:Telegrator.States.RedisStateStorage.#ctor(StackExchange.Redis.IConnectionMultiplexer)">
<summary>
Provides a Redis-based implementation of the <see cref="T:Telegrator.Core.States.IStateStorage"/> interface.
Serializes state objects to JSON format before storing them in the Redis database.
</summary>
</member>
<member name="M:Telegrator.States.RedisStateStorage.SetAsync``1(System.String,``0,System.Threading.CancellationToken)">
<inheritdoc/>
</member>
<member name="M:Telegrator.States.RedisStateStorage.GetAsync``1(System.String,System.Threading.CancellationToken)">
<inheritdoc/>
</member>
<member name="M:Telegrator.States.RedisStateStorage.DeleteAsync(System.String,System.Threading.CancellationToken)">
<inheritdoc/>
</member>
</members>
</doc>
+1 -1
View File
@@ -203,7 +203,7 @@
<see cref="T:Microsoft.Extensions.Logging.ILogger"/> of this router <see cref="T:Microsoft.Extensions.Logging.ILogger"/> of this router
</summary> </summary>
</member> </member>
<member name="M:Telegrator.Polling.HostUpdateRouter.#ctor(Telegrator.Core.IHandlersProvider,Telegrator.Core.IAwaitingProvider,Microsoft.Extensions.Options.IOptions{Telegrator.TelegratorOptions},Telegrator.Core.ITelegramBotInfo,Microsoft.Extensions.Logging.ILogger{Telegrator.Polling.HostUpdateRouter})"> <member name="M:Telegrator.Polling.HostUpdateRouter.#ctor(Telegrator.Core.IHandlersProvider,Telegrator.Core.IAwaitingProvider,Telegrator.Core.States.IStateStorage,Microsoft.Extensions.Options.IOptions{Telegrator.TelegratorOptions},Telegrator.Core.ITelegramBotInfo,Microsoft.Extensions.Logging.ILogger{Telegrator.Polling.HostUpdateRouter})">
<inheritdoc/> <inheritdoc/>
</member> </member>
<member name="M:Telegrator.Polling.HostUpdateRouter.HandleUpdateAsync(Telegram.Bot.ITelegramBotClient,Telegram.Bot.Types.Update,System.Threading.CancellationToken)"> <member name="M:Telegrator.Polling.HostUpdateRouter.HandleUpdateAsync(Telegram.Bot.ITelegramBotClient,Telegram.Bot.Types.Update,System.Threading.CancellationToken)">
+475 -890
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
# Telegrator.RedisStateStorage
**Telegrator.RedisStateStorage** is an extension for the Telegrator framework that provides Redis powered IStateStorage implementation.
---
## Requirements
- .NET standart 2.1 or later
- [Telegrator](https://github.com/Rikitav/Telegrator)
---
## Installation
```shell
dotnet add package Telegrator.RedisStateStorage
```
---
## Quick Start Example
**Program.cs:**
```csharp
using Telegrator.Hosting;
// Creating builder
TelegramBotHostBuilder builder = TelegramBotHost.CreateBuilder(new HostApplicationBuilderSettings()
{
Args = args,
ApplicationName = "TelegramBotHost example",
});
// Registerring handlers
builder.Handlers.CollectHandlersAssemblyWide();
// Register your services and
builder.Services.AddService<IStateStorage, RedisStateStorage>(services =>
new RedisStateStorage(ConnectionMultiplexer.Connect("server1:6379, server2:6379")));
// Building and running application
TelegramBotHost telegramBot = builder.Build();
telegramBot.SetBotCommands();
telegramBot.Run();
```
---
## Documentation
- [Telegrator Main Docs](https://github.com/Rikitav/Telegrator)
- [Getting Started Guide](https://github.com/Rikitav/Telegrator/wiki/Getting-started)
- [Annotation Overview](https://github.com/Rikitav/Telegrator/wiki/Annotation-overview)
---
## License
GPLv3
@@ -0,0 +1,39 @@
using StackExchange.Redis;
using System.Text.Json;
using Telegrator.Core.States;
namespace Telegrator.States;
/// <summary>
/// Provides a Redis-based implementation of the <see cref="IStateStorage"/> interface.
/// Serializes state objects to JSON format before storing them in the Redis database.
/// </summary>
public class RedisStateStorage(IConnectionMultiplexer redis) : IStateStorage
{
private readonly IDatabase _db = redis.GetDatabase();
/// <inheritdoc/>
public async Task SetAsync<T>(string key, T state, CancellationToken cancellationToken)
{
string json = JsonSerializer.Serialize(state);
await _db.StringSetAsync(key, json);
}
/// <inheritdoc/>
public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken)
{
RedisValue json = await _db.StringGetAsync(key);
string? jsonStr = json;
if (jsonStr is null)
return default;
return JsonSerializer.Deserialize<T?>(json: jsonStr);
}
/// <inheritdoc/>
public async Task DeleteAsync(string key, CancellationToken cancellationToken = default)
{
await _db.KeyDeleteAsync(key);
}
}
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<RootNamespace>Telegrator</RootNamespace>
<BaseOutputPath>..\..\bin</BaseOutputPath>
<DocumentationFile>..\..\docs\$(AssemblyName).xml</DocumentationFile>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<EnableNETAnalyzers>True</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<Title>Telegrator.RedisStateStorage</Title>
<Version>1.16.4</Version>
<Authors>Rikitav Tim4ik</Authors>
<Company>Rikitav Tim4ik</Company>
<RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl>
<PackageTags>telegram;bot;mediator;attributes;aspect;hosting;host;framework;easy;simple;handlers</PackageTags>
<PackageIcon>telegrator_nuget.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Telegrator\Telegrator.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="StackExchange.Redis" Version="2.11.8 " />
</ItemGroup>
<ItemGroup>
<None Include=".\README.md" Pack="True" PackagePath="\" />
<None Include="..\..\LICENSE" Pack="True" PackagePath="\" />
<None Include="..\..\resources\telegrator_nuget.png" Pack="True" PackagePath="\" />
</ItemGroup>
</Project>
@@ -15,7 +15,7 @@
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<Title>Telegrator.Hosting.Web</Title> <Title>Telegrator.Hosting.Web</Title>
<Version>1.16.3</Version> <Version>1.16.4</Version>
<Authors>Rikitav Tim4ik</Authors> <Authors>Rikitav Tim4ik</Authors>
<Company>Rikitav Tim4ik</Company> <Company>Rikitav Tim4ik</Company>
<RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl> <RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl>
@@ -4,6 +4,7 @@ using Telegram.Bot;
using Telegram.Bot.Polling; using Telegram.Bot.Polling;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegrator.Core; using Telegrator.Core;
using Telegrator.Core.States;
using Telegrator.Mediation; using Telegrator.Mediation;
namespace Telegrator.Polling namespace Telegrator.Polling
@@ -20,9 +21,10 @@ namespace Telegrator.Polling
public HostUpdateRouter( public HostUpdateRouter(
IHandlersProvider handlersProvider, IHandlersProvider handlersProvider,
IAwaitingProvider awaitingProvider, IAwaitingProvider awaitingProvider,
IStateStorage stateStorage,
IOptions<TelegratorOptions> options, IOptions<TelegratorOptions> options,
ITelegramBotInfo botInfo, ITelegramBotInfo botInfo,
ILogger<HostUpdateRouter> logger) : base(handlersProvider, awaitingProvider, options.Value, botInfo) ILogger<HostUpdateRouter> logger) : base(handlersProvider, awaitingProvider, stateStorage, options.Value, botInfo)
{ {
Logger = logger; Logger = logger;
ExceptionHandler = new DefaultRouterExceptionHandler(HandleException); ExceptionHandler = new DefaultRouterExceptionHandler(HandleException);
@@ -15,7 +15,7 @@
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<Title>Telegrator.Hosting</Title> <Title>Telegrator.Hosting</Title>
<Version>1.16.3</Version> <Version>1.16.4</Version>
<Authors>Rikitav Tim4ik</Authors> <Authors>Rikitav Tim4ik</Authors>
<Company>Rikitav Tim4ik</Company> <Company>Rikitav Tim4ik</Company>
<RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl> <RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl>
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
@@ -0,0 +1,32 @@
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegrator.Attributes;
using Telegrator.Core.States;
using Telegrator.Filters;
namespace Telegrator.Annotations;
/// <summary>
/// Attribute for filtering updates where resolved state matches target value.
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="value"></param>
public class StateAttribute<TKey, TValue>(TValue? value) : UpdateFilterAttribute<Update>(new StateKeyFilter<TKey, TValue>(value))
where TKey : IStateKeyResolver, new()
where TValue : IEquatable<TValue>
{
/// <summary>
/// The targetting state value.
/// </summary>
public TValue? Value => value;
/// <inheritdoc/>
public override UpdateType[] AllowedTypes => Update.AllTypes;
/// <inheritdoc/>
public override Update? GetFilterringTarget(Update update)
{
return update;
}
}
@@ -1,44 +0,0 @@
using Telegrator.StateKeeping;
using Telegrator.Attributes;
using Telegrator.Core.StateKeeping;
namespace Telegrator.Annotations.StateKeeping
{
/// <summary>
/// Attribute for managing enum-based states in Telegram bot handlers.
/// Provides a convenient way to associate enum values with state management functionality.
/// </summary>
/// <typeparam name="TEnum">The enum type to be used for state management.</typeparam>
public class EnumStateAttribute<TEnum> : StateKeeperAttribute<long, TEnum, EnumStateKeeper<TEnum>> where TEnum : Enum
{
/// <summary>
/// Initializes a new instance of the EnumStateAttribute with a special state and custom key resolver.
/// </summary>
/// <param name="specialState">The special state to be managed.</param>
/// <param name="keyResolver">The resolver for extracting keys from updates.</param>
public EnumStateAttribute(SpecialState specialState, IStateKeyResolver<long> keyResolver)
: base(specialState, keyResolver) { }
/// <summary>
/// Initializes a new instance of the EnumStateAttribute with a specific enum state and custom key resolver.
/// </summary>
/// <param name="myState">The specific enum state to be managed.</param>
/// <param name="keyResolver">The resolver for extracting keys from updates.</param>
public EnumStateAttribute(TEnum myState, IStateKeyResolver<long> keyResolver)
: base(myState, keyResolver) { }
/// <summary>
/// Initializes a new instance of the EnumStateAttribute with a special state and default sender ID resolver.
/// </summary>
/// <param name="specialState">The special state to be managed.</param>
public EnumStateAttribute(SpecialState specialState)
: base(specialState, new SenderIdResolver()) { }
/// <summary>
/// Initializes a new instance of the EnumStateAttribute with a specific enum state and default sender ID resolver.
/// </summary>
/// <param name="myState">The specific enum state to be managed.</param>
public EnumStateAttribute(TEnum myState)
: this(myState, new SenderIdResolver()) { }
}
}
@@ -1,43 +0,0 @@
using Telegrator.StateKeeping;
using Telegrator.Attributes;
using Telegrator.Core.StateKeeping;
namespace Telegrator.Annotations.StateKeeping
{
/// <summary>
/// Attribute for associating a handler or method with a numeric (integer) state keeper.
/// Provides constructors for flexible state and key resolver configuration.
/// </summary>
public class NumericStateAttribute : StateKeeperAttribute<long, int, NumericStateKeeper>
{
/// <summary>
/// Initializes the attribute with a special state and a custom key resolver.
/// </summary>
/// <param name="specialState">The special state to associate</param>
/// <param name="keyResolver">The key resolver for state keeping</param>
public NumericStateAttribute(SpecialState specialState, IStateKeyResolver<long> keyResolver)
: base(specialState, keyResolver) { }
/// <summary>
/// Initializes the attribute with a specific numeric state and a custom key resolver.
/// </summary>
/// <param name="myState">The integer state to associate</param>
/// <param name="keyResolver">The key resolver for state keeping</param>
public NumericStateAttribute(int myState, IStateKeyResolver<long> keyResolver)
: base(myState, keyResolver) { }
/// <summary>
/// Initializes the attribute with a special state and the default sender ID resolver.
/// </summary>
/// <param name="specialState">The special state to associate</param>
public NumericStateAttribute(SpecialState specialState)
: base(specialState, new SenderIdResolver()) { }
/// <summary>
/// Initializes the attribute with a specific numeric state and the default sender ID resolver.
/// </summary>
/// <param name="myState">The integer state to associate</param>
public NumericStateAttribute(int myState)
: this(myState, new SenderIdResolver()) { }
}
}
@@ -1,21 +0,0 @@
namespace Telegrator.Annotations.StateKeeping
{
/// <summary>
/// Represents special states for state keeping logic.
/// </summary>
public enum SpecialState
{
/// <summary>
/// No special state.
/// </summary>
None,
/// <summary>
/// Indicates that no state is present.
/// </summary>
NoState,
/// <summary>
/// Indicates that any state is acceptable.
/// </summary>
AnyState
}
}
@@ -1,43 +0,0 @@
using Telegrator.StateKeeping;
using Telegrator.Attributes;
using Telegrator.Core.StateKeeping;
namespace Telegrator.Annotations.StateKeeping
{
/// <summary>
/// Attribute for associating a handler or method with a string-based state keeper.
/// Provides various constructors for flexible state and key resolver configuration.
/// </summary>
public class StringStateAttribute : StateKeeperAttribute<long, string, StringStateKeeper>
{
/// <summary>
/// Initializes the attribute with a special state and a custom key resolver.
/// </summary>
/// <param name="specialState">The special state to associate</param>
/// <param name="keyResolver">The key resolver for state keeping</param>
public StringStateAttribute(SpecialState specialState, IStateKeyResolver<long> keyResolver)
: base(specialState, keyResolver) { }
/// <summary>
/// Initializes the attribute with a specific state and a custom key resolver.
/// </summary>
/// <param name="myState">The string state to associate</param>
/// <param name="keyResolver">The key resolver for state keeping</param>
public StringStateAttribute(string myState, IStateKeyResolver<long> keyResolver)
: base(myState, keyResolver) { }
/// <summary>
/// Initializes the attribute with a special state and the default sender ID resolver.
/// </summary>
/// <param name="specialState">The special state to associate</param>
public StringStateAttribute(SpecialState specialState)
: base(specialState, new SenderIdResolver()) { }
/// <summary>
/// Initializes the attribute with a specific state and the default sender ID resolver.
/// </summary>
/// <param name="myState">The string state to associate</param>
public StringStateAttribute(string myState)
: base(myState, new SenderIdResolver()) { }
}
}
@@ -2,7 +2,7 @@
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using Telegrator.Filters; using Telegrator.Filters;
namespace Telegrator.Annotations.Targetted namespace Telegrator.Annotations
{ {
/// <summary> /// <summary>
/// Attribute for filtering message with command "start" in bot's private chats. /// Attribute for filtering message with command "start" in bot's private chats.
@@ -1,86 +0,0 @@
using Telegram.Bot.Types;
using Telegrator.Annotations.StateKeeping;
using Telegrator.Core.Attributes;
using Telegrator.Core.Filters;
using Telegrator.Core.StateKeeping;
namespace Telegrator.Attributes
{
/// <summary>
/// Abstract attribute for associating a handler or method with a state keeper.
/// Provides logic for state-based filtering and state management.
/// </summary>
/// <typeparam name="TKey">The type of the key used for state keeping (e.g., chat ID).</typeparam>
/// <typeparam name="TState">The type of the state value (e.g., string, int).</typeparam>
/// <typeparam name="TKeeper">The type of the state keeper implementation.</typeparam>
public abstract class StateKeeperAttribute<TKey, TState, TKeeper> : StateKeeperAttributeBase where TKey : notnull where TState : notnull where TKeeper : StateKeeperBase<TKey, TState>, new()
{
/*
private static readonly TKeeper _shared = new TKeeper();
private static readonly Dictionary<TKey, TKeeper> _keyed = [];
*/
/// <summary>
/// Gets or sets the singleton instance of the state keeper for this attribute type.
/// </summary>
public static TKeeper Shared { get; } = new TKeeper();
/// <summary>
/// Gets the default state value of this statekeeper.
/// </summary>
public static TState DefaultState => Shared.DefaultState;
/// <summary>
/// Gets the state value associated with this attribute instance.
/// </summary>
public TState MyState { get; private set; }
/// <summary>
/// Gets the special state mode for this attribute instance.
/// </summary>
public SpecialState SpecialState { get; private set; }
/// <summary>
/// Initializes the attribute with a specific state and a custom key resolver.
/// </summary>
/// <param name="myState">The state value to associate</param>
/// <param name="keyResolver">The key resolver for state keeping</param>
protected StateKeeperAttribute(TState myState, IStateKeyResolver<TKey> keyResolver) : base(typeof(TKeeper))
{
Shared.KeyResolver = keyResolver;
MyState = myState;
SpecialState = SpecialState.None;
}
/// <summary>
/// Initializes the attribute with a special state and a custom key resolver.
/// </summary>
/// <param name="specialState">The special state mode</param>
/// <param name="keyResolver">The key resolver for state keeping</param>
protected StateKeeperAttribute(SpecialState specialState, IStateKeyResolver<TKey> keyResolver) : base(typeof(TKeeper))
{
Shared.KeyResolver = keyResolver;
MyState = Shared.DefaultState;
SpecialState = specialState;
}
/// <summary>
/// Determines whether the current update context passes the state filter.
/// </summary>
/// <param name="context">The filter execution context</param>
/// <returns>True if the state matches the filter; otherwise, false.</returns>
public override bool CanPass(FilterExecutionContext<Update> context)
{
if (SpecialState == SpecialState.AnyState)
return true;
if (!Shared.TryGetState(context.Input, out TState? state))
return SpecialState == SpecialState.NoState;
if (state == null)
return false;
return MyState.Equals(state);
}
}
}
@@ -1,35 +0,0 @@
using Telegram.Bot.Types;
using Telegrator.Core.Filters;
using Telegrator.Core.Handlers;
using Telegrator.Core.StateKeeping;
namespace Telegrator.Core.Attributes
{
/// <summary>
/// Sets the state in which the <see cref="UpdateHandlerBase"/> can be executed
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public abstract class StateKeeperAttributeBase : Attribute, IFilter<Update>
{
/// <inheritdoc/>
public bool IsCollectible => GetType().HasPublicProperties();
/// <summary>
/// Creates a new instance <see cref="StateKeeperBase{TKey, TState}"/>
/// </summary>
/// <param name="stateKeeperType"></param>
/// <exception cref="ArgumentException"></exception>
protected StateKeeperAttributeBase(Type stateKeeperType)
{
if (!stateKeeperType.IsAssignableToGenericType(typeof(StateKeeperBase<,>)))
throw new ArgumentException(stateKeeperType + " is not a StateKeeperBase", nameof(stateKeeperType));
}
/// <summary>
/// Realizes a <see cref="IFilter{T}"/> for validation of the current <see cref="StateKeeperBase{TKey, TState}"/> in the polling routing
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public abstract bool CanPass(FilterExecutionContext<Update> context);
}
}
@@ -24,7 +24,7 @@ namespace Telegrator.Core.Descriptors
public MethodHandlerDescriptor(AbstractHandlerAction<TUpdate> action) : base(DescriptorType.General, typeof(MethodHandler), true) public MethodHandlerDescriptor(AbstractHandlerAction<TUpdate> action) : base(DescriptorType.General, typeof(MethodHandler), true)
{ {
UpdateHandlerAttributeBase handlerAttribute = HandlerInspector.GetHandlerAttribute(action.Method); UpdateHandlerAttributeBase handlerAttribute = HandlerInspector.GetHandlerAttribute(action.Method);
StateKeeperAttributeBase? stateKeeperAttribute = HandlerInspector.GetStateKeeperAttribute(action.Method); IFilter<Update>? stateKeeperAttribute = HandlerInspector.GetStateKeeperAttribute(action.Method);
IFilter<Update>[] filters = HandlerInspector.GetFilterAttributes(action.Method, handlerAttribute.Type).ToArray(); IFilter<Update>[] filters = HandlerInspector.GetFilterAttributes(action.Method, handlerAttribute.Type).ToArray();
UpdateType = handlerAttribute.Type; UpdateType = handlerAttribute.Type;
@@ -2,6 +2,7 @@
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.Handlers; using Telegrator.Core.Handlers;
using Telegrator.Core.States;
namespace Telegrator.Core.Descriptors namespace Telegrator.Core.Descriptors
{ {
@@ -27,6 +28,11 @@ namespace Telegrator.Core.Descriptors
/// </summary> /// </summary>
public IAwaitingProvider AwaitingProvider { get; } public IAwaitingProvider AwaitingProvider { get; }
/// <summary>
/// The state storage to handling state machines
/// </summary>
public IStateStorage StateStorage { get; }
/// <summary> /// <summary>
/// The Telegram bot client used for this handler. /// The Telegram bot client used for this handler.
/// </summary> /// </summary>
@@ -73,6 +79,7 @@ namespace Telegrator.Core.Descriptors
/// <param name="fromDescriptor">The descriptor from which this handler was described.</param> /// <param name="fromDescriptor">The descriptor from which this handler was described.</param>
/// <param name="updateRouter">The update router.</param> /// <param name="updateRouter">The update router.</param>
/// <param name="awaitingProvider">The awaiting provider.</param> /// <param name="awaitingProvider">The awaiting provider.</param>
/// <param name="stateStorage">The state storage.</param>
/// <param name="client">The Telegram bot client.</param> /// <param name="client">The Telegram bot client.</param>
/// <param name="handlerInstance">The handler instance.</param> /// <param name="handlerInstance">The handler instance.</param>
/// <param name="filterContext">The filter execution context.</param> /// <param name="filterContext">The filter execution context.</param>
@@ -81,6 +88,7 @@ namespace Telegrator.Core.Descriptors
HandlerDescriptor fromDescriptor, HandlerDescriptor fromDescriptor,
IUpdateRouter updateRouter, IUpdateRouter updateRouter,
IAwaitingProvider awaitingProvider, IAwaitingProvider awaitingProvider,
IStateStorage stateStorage,
ITelegramBotClient client, ITelegramBotClient client,
UpdateHandlerBase handlerInstance, UpdateHandlerBase handlerInstance,
FilterExecutionContext<Update> filterContext, FilterExecutionContext<Update> filterContext,
@@ -89,6 +97,7 @@ namespace Telegrator.Core.Descriptors
From = fromDescriptor; From = fromDescriptor;
UpdateRouter = updateRouter; UpdateRouter = updateRouter;
AwaitingProvider = awaitingProvider; AwaitingProvider = awaitingProvider;
StateStorage = stateStorage;
Client = client; Client = client;
HandlerInstance = handlerInstance; HandlerInstance = handlerInstance;
ExtraData = filterContext.Data; ExtraData = filterContext.Data;
@@ -167,7 +167,7 @@ namespace Telegrator.Core.Descriptors
if (handlerAttribute.ExpectingHandlerType != null && !handlerAttribute.ExpectingHandlerType.Contains(handlerType.BaseType)) if (handlerAttribute.ExpectingHandlerType != null && !handlerAttribute.ExpectingHandlerType.Contains(handlerType.BaseType))
throw new ArgumentException(string.Format("This handler attribute cannot be attached to this class. Attribute can be attached on next handlers : {0}", string.Join(", ", handlerAttribute.ExpectingHandlerType.AsEnumerable()))); throw new ArgumentException(string.Format("This handler attribute cannot be attached to this class. Attribute can be attached on next handlers : {0}", string.Join(", ", handlerAttribute.ExpectingHandlerType.AsEnumerable())));
StateKeeperAttributeBase? stateKeeperAttribute = HandlerInspector.GetStateKeeperAttribute(handlerType); IFilter<Update>? stateKeeperAttribute = HandlerInspector.GetStateKeeperAttribute(handlerType);
IFilter<Update>[] filters = HandlerInspector.GetFilterAttributes(handlerType, handlerAttribute.Type).ToArray(); IFilter<Update>[] filters = HandlerInspector.GetFilterAttributes(handlerType, handlerAttribute.Type).ToArray();
UpdateType = handlerAttribute.Type; UpdateType = handlerAttribute.Type;
@@ -2,6 +2,7 @@
using System.Reflection; using System.Reflection;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using Telegrator.Annotations;
using Telegrator.Aspects; using Telegrator.Aspects;
using Telegrator.Core.Attributes; using Telegrator.Core.Attributes;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
@@ -42,13 +43,13 @@ namespace Telegrator.Core.Descriptors
/// </summary> /// </summary>
/// <param name="handlerType">The member info representing the handler type.</param> /// <param name="handlerType">The member info representing the handler type.</param>
/// <returns>The state keeper attribute, or null if not present.</returns> /// <returns>The state keeper attribute, or null if not present.</returns>
public static StateKeeperAttributeBase? GetStateKeeperAttribute(MemberInfo handlerType) public static IFilter<Update>? GetStateKeeperAttribute(MemberInfo handlerType)
{ {
// Getting polling handler attribute // Getting polling handler attribute
IEnumerable<StateKeeperAttributeBase> handlerAttrs = handlerType.GetCustomAttributes<StateKeeperAttributeBase>(); Attribute stateAttr = handlerType.GetCustomAttribute(typeof(StateAttribute<,>));
// //
return handlerAttrs.Any() ? handlerAttrs.Single() : null; return stateAttr as IFilter<Update>;
} }
/// <summary> /// <summary>
@@ -9,6 +9,11 @@ namespace Telegrator.Core.Filters
/// <typeparam name="T">The type of the input for the filter.</typeparam> /// <typeparam name="T">The type of the input for the filter.</typeparam>
public class FilterExecutionContext<T> where T : class public class FilterExecutionContext<T> where T : class
{ {
/// <summary>
/// Gets the <see cref="ITelegramBotInfo"/> for the current context.
/// </summary>
public IUpdateRouter UpdateRouter { get; }
/// <summary> /// <summary>
/// Gets the <see cref="ITelegramBotInfo"/> for the current context. /// Gets the <see cref="ITelegramBotInfo"/> for the current context.
/// </summary> /// </summary>
@@ -42,13 +47,15 @@ namespace Telegrator.Core.Filters
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FilterExecutionContext{T}"/> class with all parameters. /// Initializes a new instance of the <see cref="FilterExecutionContext{T}"/> class with all parameters.
/// </summary> /// </summary>
/// <param name="router">The router, that invoked filter.</param>
/// <param name="botInfo">The bot info.</param> /// <param name="botInfo">The bot info.</param>
/// <param name="update">The update.</param> /// <param name="update">The update.</param>
/// <param name="input">The input object.</param> /// <param name="input">The input object.</param>
/// <param name="data">The additional data dictionary.</param> /// <param name="data">The additional data dictionary.</param>
/// <param name="completedFilters">The list of completed filters.</param> /// <param name="completedFilters">The list of completed filters.</param>
public FilterExecutionContext(ITelegramBotInfo botInfo, Update update, T input, Dictionary<string, object> data, CompletedFiltersList completedFilters) public FilterExecutionContext(IUpdateRouter router, ITelegramBotInfo botInfo, Update update, T input, Dictionary<string, object> data, CompletedFiltersList completedFilters)
{ {
UpdateRouter = router;
BotInfo = botInfo; BotInfo = botInfo;
Data = data; Data = data;
CompletedFilters = completedFilters; CompletedFilters = completedFilters;
@@ -60,11 +67,12 @@ namespace Telegrator.Core.Filters
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FilterExecutionContext{T}"/> class with default data and filters. /// Initializes a new instance of the <see cref="FilterExecutionContext{T}"/> class with default data and filters.
/// </summary> /// </summary>
/// <param name="router">The router, that invoked filter.</param>
/// <param name="botInfo">The bot info.</param> /// <param name="botInfo">The bot info.</param>
/// <param name="update">The update.</param> /// <param name="update">The update.</param>
/// <param name="input">The input object.</param> /// <param name="input">The input object.</param>
public FilterExecutionContext(ITelegramBotInfo botInfo, Update update, T input) public FilterExecutionContext(IUpdateRouter router, ITelegramBotInfo botInfo, Update update, T input)
: this(botInfo, update, input, [], []) { } : this(router, botInfo, update, input, [], []) { }
/// <summary> /// <summary>
/// Creates a child context for a different input type, sharing the same data and completed filters. /// Creates a child context for a different input type, sharing the same data and completed filters.
@@ -73,6 +81,6 @@ namespace Telegrator.Core.Filters
/// <param name="input">The new input object.</param> /// <param name="input">The new input object.</param>
/// <returns>A new <see cref="FilterExecutionContext{C}"/> instance.</returns> /// <returns>A new <see cref="FilterExecutionContext{C}"/> instance.</returns>
public FilterExecutionContext<C> CreateChild<C>(C input) where C : class public FilterExecutionContext<C> CreateChild<C>(C input) where C : class
=> new FilterExecutionContext<C>(BotInfo, Update, input, Data, CompletedFilters); => new FilterExecutionContext<C>(UpdateRouter, BotInfo, Update, input, Data, CompletedFilters);
} }
} }
@@ -3,6 +3,7 @@ using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using Telegrator.Core.Descriptors; using Telegrator.Core.Descriptors;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.States;
using Telegrator.Handlers; using Telegrator.Handlers;
namespace Telegrator.Core.Handlers namespace Telegrator.Core.Handlers
@@ -47,6 +48,11 @@ namespace Telegrator.Core.Handlers
/// </summary> /// </summary>
protected IAwaitingProvider AwaitingProvider => Container.AwaitingProvider; protected IAwaitingProvider AwaitingProvider => Container.AwaitingProvider;
/// <summary>
/// Storage of bot states.
/// </summary>
protected IStateStorage StateStorage => Container.StateStorage;
/// <summary> /// <summary>
/// Initializes a new instance and checks that the update type matches <typeparamref name="TUpdate"/>. /// Initializes a new instance and checks that the update type matches <typeparamref name="TUpdate"/>.
/// </summary> /// </summary>
@@ -1,9 +1,9 @@
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using Telegrator.Annotations.StateKeeping;
using Telegrator.Core.Descriptors; using Telegrator.Core.Descriptors;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.StateKeeping; using Telegrator.Core.States;
using Telegrator.Filters;
namespace Telegrator.Core.Handlers.Building namespace Telegrator.Core.Handlers.Building
{ {
@@ -140,35 +140,15 @@ namespace Telegrator.Core.Handlers.Building
/// <summary> /// <summary>
/// Sets a state keeper for the handler using a specific state and key resolver. /// Sets a state keeper for the handler using a specific state and key resolver.
/// </summary> /// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam> /// <typeparam name="TKey">The key resolver.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam> /// <typeparam name="TValue">The state value.</typeparam>
/// <typeparam name="TKeeper">The type of the state keeper.</typeparam> /// <param name="state">The state value.</param>
/// <param name="myState">The state value.</param>
/// <param name="keyResolver">The key resolver.</param>
/// <returns>The builder instance.</returns> /// <returns>The builder instance.</returns>
public void SetStateKeeper<TKey, TState, TKeeper>(TState myState, IStateKeyResolver<TKey> keyResolver) public void SetState<TKey, TValue>(TValue? state)
where TKey : notnull where TKey : IStateKeyResolver, new()
where TState : IEquatable<TState> where TValue : IEquatable<TValue>
where TKeeper : StateKeeperBase<TKey, TState>, new()
{ {
StateKeeper = new StateKeepFilter<TKey, TState, TKeeper>(myState, keyResolver); StateKeeper = new StateKeyFilter<TKey, TValue>(state);
}
/// <summary>
/// Sets a state keeper for the handler using a special state and key resolver.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam>
/// <typeparam name="TKeeper">The type of the state keeper.</typeparam>
/// <param name="specialState">The special state value.</param>
/// <param name="keyResolver">The key resolver.</param>
/// <returns>The builder instance.</returns>
public void SetStateKeeper<TKey, TState, TKeeper>(SpecialState specialState, IStateKeyResolver<TKey> keyResolver)
where TKey : notnull
where TState : IEquatable<TState>
where TKeeper : StateKeeperBase<TKey, TState>, new()
{
StateKeeper = new StateKeepFilter<TKey, TState, TKeeper>(specialState, keyResolver);
} }
/// <summary> /// <summary>
@@ -1,4 +1,4 @@
using Telegrator.Core.StateKeeping; using Telegrator.Core.States;
namespace Telegrator.Core.Handlers.Building namespace Telegrator.Core.Handlers.Building
{ {
@@ -11,9 +11,9 @@ namespace Telegrator.Core.Handlers.Building
/// <summary> /// <summary>
/// Awaits an update using the specified key resolver and cancellation token. /// Awaits an update using the specified key resolver and cancellation token.
/// </summary> /// </summary>
/// <param name="keyResolver">The <see cref="IStateKeyResolver{TKey}"/> to resolve the key.</param> /// <param name="keyResolver">The <see cref="IStateKeyResolver"/> to resolve the key.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TUpdate}"/> representing the awaited update.</returns> /// <returns>A <see cref="Task{TUpdate}"/> representing the awaited update.</returns>
public Task<TUpdate> Await(IStateKeyResolver<long> keyResolver, CancellationToken cancellationToken = default); public Task<TUpdate> Await(IStateKeyResolver keyResolver, CancellationToken cancellationToken = default);
} }
} }
@@ -1,7 +1,6 @@
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegrator.Annotations.StateKeeping;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.StateKeeping; using Telegrator.Core.States;
namespace Telegrator.Core.Handlers.Building namespace Telegrator.Core.Handlers.Building
{ {
@@ -56,30 +55,13 @@ namespace Telegrator.Core.Handlers.Building
/// <summary> /// <summary>
/// Sets a state keeper for the handler using a specific state and key resolver. /// Sets a state keeper for the handler using a specific state and key resolver.
/// </summary> /// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam> /// <typeparam name="TKey">The key resolver.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam> /// <typeparam name="TValue">The state value.</typeparam>
/// <typeparam name="TKeeper">The type of the state keeper.</typeparam> /// <param name="state">The state value.</param>
/// <param name="myState">The state value.</param>
/// <param name="keyResolver">The key resolver.</param>
/// <returns>The builder instance.</returns> /// <returns>The builder instance.</returns>
public void SetStateKeeper<TKey, TState, TKeeper>(TState myState, IStateKeyResolver<TKey> keyResolver) public void SetState<TKey, TValue>(TValue? state)
where TKey : notnull where TKey : IStateKeyResolver, new()
where TState : IEquatable<TState> where TValue : IEquatable<TValue>;
where TKeeper : StateKeeperBase<TKey, TState>, new();
/// <summary>
/// Sets a state keeper for the handler using a special state and key resolver.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam>
/// <typeparam name="TKeeper">The type of the state keeper.</typeparam>
/// <param name="specialState">The special state value.</param>
/// <param name="keyResolver">The key resolver.</param>
/// <returns>The builder instance.</returns>
public void SetStateKeeper<TKey, TState, TKeeper>(SpecialState specialState, IStateKeyResolver<TKey> keyResolver)
where TKey : notnull
where TState : IEquatable<TState>
where TKeeper : StateKeeperBase<TKey, TState>, new();
/// <summary> /// <summary>
/// Adds a targeted filter for a specific filter target type. /// Adds a targeted filter for a specific filter target type.
@@ -1,80 +0,0 @@
using Telegram.Bot.Types;
using Telegrator.Annotations.StateKeeping;
using Telegrator.Core.Filters;
using Telegrator.Core.StateKeeping;
using Telegrator.Filters;
namespace Telegrator.Core.Handlers.Building
{
/// <summary>
/// Filter for state keeping logic, allowing filtering based on state and special state conditions.
/// </summary>
/// <typeparam name="TKey">The type of the key for state resolution.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam>
/// <typeparam name="TKeeper">The type of the state keeper.</typeparam>
public class StateKeepFilter<TKey, TState, TKeeper> : Filter<Update>
where TKey : notnull
where TState : IEquatable<TState>
where TKeeper : StateKeeperBase<TKey, TState>, new()
{
/// <summary>
/// Gets or sets the state keeper instance.
/// </summary>
public static TKeeper StateKeeper { get; internal set; } = null!;
/// <summary>
/// Gets the state value for this filter.
/// </summary>
public TState MyState { get; private set; }
/// <summary>
/// Gets the special state value for this filter.
/// </summary>
public SpecialState SpecialState { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="StateKeepFilter{TKey, TState, TKeeper}"/> class with a specific state.
/// </summary>
/// <param name="myState">The state value.</param>
/// <param name="keyResolver">The key resolver.</param>
public StateKeepFilter(TState myState, IStateKeyResolver<TKey> keyResolver)
{
StateKeeper ??= new TKeeper();
StateKeeper.KeyResolver = keyResolver;
MyState = myState;
SpecialState = SpecialState.None;
}
/// <summary>
/// Initializes a new instance of the <see cref="StateKeepFilter{TKey, TState, TKeeper}"/> class with a special state.
/// </summary>
/// <param name="specialState">The special state value.</param>
/// <param name="keyResolver">The key resolver.</param>
public StateKeepFilter(SpecialState specialState, IStateKeyResolver<TKey> keyResolver)
{
StateKeeper ??= new TKeeper();
StateKeeper.KeyResolver = keyResolver;
MyState = StateKeeper.DefaultState;
SpecialState = specialState;
}
/// <summary>
/// Determines whether the filter can pass for the given context based on state logic.
/// </summary>
/// <param name="context">The filter execution context.</param>
/// <returns>True if the filter passes; otherwise, false.</returns>
public override bool CanPass(FilterExecutionContext<Update> context)
{
if (SpecialState == SpecialState.AnyState)
return true;
if (!StateKeeper.TryGetState(context.Input, out TState? state))
return SpecialState == SpecialState.NoState;
if (state == null)
return false;
return MyState.Equals(state);
}
}
}
@@ -1,6 +1,7 @@
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.States;
namespace Telegrator.Core.Handlers namespace Telegrator.Core.Handlers
{ {
@@ -23,5 +24,8 @@ namespace Telegrator.Core.Handlers
/// <inheritdoc/> /// <inheritdoc/>
public IAwaitingProvider AwaitingProvider => throw new NotImplementedException(); public IAwaitingProvider AwaitingProvider => throw new NotImplementedException();
/// <inheritdoc/>
public IStateStorage StateStorage => throw new NotImplementedException();
} }
} }
@@ -1,6 +1,7 @@
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.States;
namespace Telegrator.Core.Handlers namespace Telegrator.Core.Handlers
{ {
@@ -34,5 +35,10 @@ namespace Telegrator.Core.Handlers
/// Gets the <see cref="IAwaitingProvider"/> for awaiting operations. /// Gets the <see cref="IAwaitingProvider"/> for awaiting operations.
/// </summary> /// </summary>
public IAwaitingProvider AwaitingProvider { get; } public IAwaitingProvider AwaitingProvider { get; }
/// <summary>
/// Gets the <see cref="IStateStorage"/> for state managment.
/// </summary>
public IStateStorage StateStorage { get; }
} }
} }
@@ -125,7 +125,7 @@ namespace Telegrator.Core.Handlers
} }
} }
internal IHandlerContainer GetContainer(DescribedHandlerDescriptor handlerInfo) private IHandlerContainer GetContainer(DescribedHandlerDescriptor handlerInfo)
{ {
if (this is IHandlerContainerFactory handlerDefainedContainerFactory) if (this is IHandlerContainerFactory handlerDefainedContainerFactory)
return handlerDefainedContainerFactory.CreateContainer(handlerInfo); return handlerDefainedContainerFactory.CreateContainer(handlerInfo);
-19
View File
@@ -1,19 +0,0 @@
namespace Telegrator.Core
{
/// <summary>
/// Interface for polling providers that manage both regular and awaiting handlers.
/// Provides access to handlers for different types of update processing during polling operations.
/// </summary>
public interface IPollingProvider
{
/// <summary>
/// Gets the <see cref="IHandlersProvider"/> that manages handlers for polling.
/// </summary>
public IHandlersProvider HandlersProvider { get; }
/// <summary>
/// Gets the <see cref="IAwaitingProvider"/> that manages awaiting handlers for polling.
/// </summary>
public IAwaitingProvider AwaitingProvider { get; }
}
}
+17 -1
View File
@@ -1,5 +1,6 @@
using Telegram.Bot.Polling; using Telegram.Bot.Polling;
using Telegrator.Core.Handlers; using Telegrator.Core.Handlers;
using Telegrator.Core.States;
namespace Telegrator.Core namespace Telegrator.Core
{ {
@@ -7,7 +8,7 @@ namespace Telegrator.Core
/// Interface for update routers that handle incoming updates and manage handler execution. /// Interface for update routers that handle incoming updates and manage handler execution.
/// Combines update handling capabilities with polling provider functionality and exception handling. /// Combines update handling capabilities with polling provider functionality and exception handling.
/// </summary> /// </summary>
public interface IUpdateRouter : IUpdateHandler, IPollingProvider public interface IUpdateRouter : IUpdateHandler
{ {
/// <summary> /// <summary>
/// Gets the <see cref="TelegratorOptions"/> for the router. /// Gets the <see cref="TelegratorOptions"/> for the router.
@@ -19,6 +20,21 @@ namespace Telegrator.Core
/// </summary> /// </summary>
public IUpdateHandlersPool HandlersPool { get; } public IUpdateHandlersPool HandlersPool { get; }
/// <summary>
/// Gets the <see cref="IHandlersProvider"/> that manages handlers for polling.
/// </summary>
public IHandlersProvider HandlersProvider { get; }
/// <summary>
/// Gets the <see cref="IAwaitingProvider"/> that manages awaiting handlers for polling.
/// </summary>
public IAwaitingProvider AwaitingProvider { get; }
/// <summary>
/// Gets the <see cref="IStateStorage"/> that manages storing of handlers state.
/// </summary>
public IStateStorage StateStorage { get; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="IRouterExceptionHandler"/> for handling exceptions. /// Gets or sets the <see cref="IRouterExceptionHandler"/> for handling exceptions.
/// </summary> /// </summary>
@@ -1,18 +0,0 @@
using Telegram.Bot.Types;
namespace Telegrator.Core.StateKeeping
{
/// <summary>
/// Defines a resolver for extracting a key from an update for state keeping purposes.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
public interface IStateKeyResolver<TKey> where TKey : notnull
{
/// <summary>
/// Resolves a key from the specified <see cref="Update"/>.
/// </summary>
/// <param name="keySource">The update to resolve the key from.</param>
/// <returns>The resolved key.</returns>
public TKey ResolveKey(Update keySource);
}
}
@@ -1,150 +0,0 @@
using Telegram.Bot.Types;
namespace Telegrator.Core.StateKeeping
{
/// <summary>
/// Base class for managing state associated with updates and keys.
/// </summary>
/// <typeparam name="TKey">The type of the key used for state resolution.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam>
public abstract class StateKeeperBase<TKey, TState> where TState : notnull where TKey : notnull
{
private readonly Dictionary<TKey, TState> States = [];
/// <summary>
/// Gets or sets the key resolver used to resolve keys from updates.
/// </summary>
public IStateKeyResolver<TKey> KeyResolver { get; set; } = null!;
/// <summary>
/// Gets the default state value.
/// </summary>
public abstract TState DefaultState { get; }
/// <summary>
/// Sets the state for the specified update.
/// </summary>
/// <param name="keySource">The update to use as a key source.</param>
/// <param name="newState">The new state value.</param>
public virtual void SetState(Update keySource, TState newState)
{
TKey key = KeyResolver.ResolveKey(keySource);
States.Set(key, newState, DefaultState);
}
/// <summary>
/// Gets the state for the specified update.
/// </summary>
/// <param name="keySource">The update to use as a key source.</param>
/// <returns>The state value.</returns>
public virtual TState GetState(Update keySource)
{
TKey key = KeyResolver.ResolveKey(keySource);
return States[key];
}
/// <summary>
/// Tries to get the state for the specified update.
/// </summary>
/// <param name="keySource">The update to use as a key source.</param>
/// <param name="state">When this method returns, contains the state value if found; otherwise, the default value.</param>
/// <returns>True if the state was found; otherwise, false.</returns>
public virtual bool TryGetState(Update keySource, out TState? state)
{
TKey key = KeyResolver.ResolveKey(keySource);
return States.TryGetValue(key, out state);
}
/// <summary>
/// Determines whether a state exists for the specified update.
/// </summary>
/// <param name="keySource">The update to use as a key source.</param>
/// <returns>True if the state exists; otherwise, false.</returns>
public virtual bool HasState(Update keySource)
{
TKey key = KeyResolver.ResolveKey(keySource);
return States.ContainsKey(key);
}
/// <summary>
/// Creates a state for the specified update using the default state value.
/// </summary>
/// <param name="keySource">The update to use as a key source.</param>
public virtual void CreateState(Update keySource)
{
TKey key = KeyResolver.ResolveKey(keySource);
States.Set(key, DefaultState);
}
/// <summary>
/// Deletes the state for the specified update.
/// </summary>
/// <param name="keySource">The update to use as a key source.</param>
public virtual void DeleteState(Update keySource)
{
TKey key = KeyResolver.ResolveKey(keySource);
States.Remove(key);
}
/// <summary>
/// Moves the state forward for the specified update.
/// </summary>
/// <param name="keySource">The update to use as a key source.</param>
public virtual void MoveForward(Update keySource)
{
TKey key = KeyResolver.ResolveKey(keySource);
if (!States.TryGetValue(key, out TState currentState))
{
States.Set(key, DefaultState);
currentState = DefaultState;
}
TState newState = MoveForward(currentState, key);
States[key] = newState;
}
/// <summary>
/// Moves the state backward for the specified update.
/// </summary>
/// <param name="keySource">The update to use as a key source.</param>
public virtual void MoveBackward(Update keySource)
{
TKey key = KeyResolver.ResolveKey(keySource);
if (!States.TryGetValue(key, out TState currentState))
{
States.Set(key, DefaultState);
return;
}
TState newState = MoveBackward(currentState, key);
States[key] = newState;
}
/*
/// <summary>
/// Gets the state keeper for the specified key.
/// </summary>
/// <typeparam name="TStateKeeper">The type of the state keeper.</typeparam>
/// <param name="key">The key.</param>
/// <returns>The state keeper instance.</returns>
protected virtual TStateKeeper GetKeeper<TStateKeeper>(TKey key) where TStateKeeper : StateKeeperBase<TKey, TState>
=> States[key] as TStateKeeper ?? throw new InvalidCastException();
*/
/// <summary>
/// Moves the state forward for the specified current state and key.
/// </summary>
/// <param name="currentState">The current state value.</param>
/// <param name="currentKey">The key.</param>
/// <returns>The new state value.</returns>
protected abstract TState MoveForward(TState currentState, TKey currentKey);
/// <summary>
/// Moves the state backward for the specified current state and key.
/// </summary>
/// <param name="currentState">The current state value.</param>
/// <param name="currentKey">The key.</param>
/// <returns>The new state value.</returns>
protected abstract TState MoveBackward(TState currentState, TKey currentKey);
}
}
@@ -0,0 +1,16 @@
using Telegram.Bot.Types;
namespace Telegrator.Core.States;
/// <summary>
/// Defines a resolver for extracting a key from an update for state keeping purposes.
/// </summary>
public interface IStateKeyResolver
{
/// <summary>
/// Resolves a key from the specified <see cref="Update"/>.
/// </summary>
/// <param name="keySource">The update to resolve the key from.</param>
/// <returns>The resolved key.</returns>
public string? ResolveKey(Update keySource);
}
@@ -0,0 +1,44 @@
namespace Telegrator.Core.States;
/// <summary>
/// Defines a contract for a state machine that manages transitions and retrieves states for specific updates.
/// </summary>
/// <typeparam name="TState">The type of the state. Must implement <see cref="IEquatable{T}"/>.</typeparam>
public interface IStateMachine<TState> where TState : IEquatable<TState>
{
/// <summary>
/// Gets the current state associated with the specified update key.
/// </summary>
/// <param name="storage">The storage mechanism used to persist the state.</param>
/// <param name="updateKey">The unique key identifying the current update context (e.g., chat and user ID).</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the current state, or the default value if no state is found.</returns>
Task<TState?> Current(IStateStorage storage, string updateKey, CancellationToken cancellationToken = default);
/// <summary>
/// Advances the state machine to the next state in the sequence.
/// </summary>
/// <param name="storage">The storage mechanism used to persist the state.</param>
/// <param name="updateKey">The unique key identifying the current update context.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the asynchronous transition operation.</returns>
Task Advance(IStateStorage storage, string updateKey, CancellationToken cancellationToken = default);
/// <summary>
/// Moves the state machine backward to the previous state in the sequence.
/// </summary>
/// <param name="storage">The storage mechanism used to persist the state.</param>
/// <param name="updateKey">The unique key identifying the current update context.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the asynchronous transition operation.</returns>
Task Retreat(IStateStorage storage, string updateKey, CancellationToken cancellationToken = default);
/// <summary>
/// Resets the state machine to its initial or default state.
/// </summary>
/// <param name="storage">The storage mechanism used to persist the state.</param>
/// <param name="updateKey">The unique key identifying the current update context.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the asynchronous reset operation.</returns>
Task Reset(IStateStorage storage, string updateKey, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,34 @@
namespace Telegrator.Core.States;
/// <summary>
/// Defines a contract for an asynchronous state storage mechanism.
/// </summary>
public interface IStateStorage
{
/// <summary>
/// Saves or updates a state value associated with the specified key.
/// </summary>
/// <typeparam name="T">The type of the state object.</typeparam>
/// <param name="key">The unique identifier for the state.</param>
/// <param name="state">The state object to store.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
Task SetAsync<T>(string key, T state, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a state value associated with the specified key.
/// </summary>
/// <typeparam name="T">The type of the state object to retrieve.</typeparam>
/// <param name="key">The unique identifier for the state.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the asynchronous retrieve operation. The task result contains the state object if found; otherwise, the default value of <typeparamref name="T"/>.</returns>
Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes the state value associated with the specified key.
/// </summary>
/// <param name="key">The unique identifier for the state to remove.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the asynchronous delete operation.</returns>
Task DeleteAsync(string key, CancellationToken cancellationToken = default);
}
+23 -10
View File
@@ -1,31 +1,44 @@
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.StateKeeping; using Telegrator.Core.States;
namespace Telegrator.Filters namespace Telegrator.Filters
{ {
/// <summary> /// <summary>
/// Filters updates by comparing a resolved state key with a target key. /// Filters updates by comparing a resolved state key with a target key.
/// </summary> /// </summary>
/// <typeparam name="TKey">The type of the key used for state resolution.</typeparam> /// <typeparam name="TKey">The type of the key resolver used to get state key.</typeparam>
public class StateKeyFilter<TKey> : Filter<Update> where TKey : IEquatable<TKey> /// <typeparam name="TValue">The type of the key used for state resolution.</typeparam>
public class StateKeyFilter<TKey, TValue> : Filter<Update>
where TKey : IStateKeyResolver, new()
where TValue : IEquatable<TValue>
{ {
private readonly IStateKeyResolver<TKey> KeyResolver; private readonly TValue? TargetKey;
private readonly TKey TargetKey;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StateKeyFilter{TKey}"/> class. /// Initializes a new instance of the <see cref="StateKeyFilter{TKey, TValue}"/> class.
/// </summary> /// </summary>
/// <param name="keyResolver">The key resolver to extract the key from the update.</param>
/// <param name="targetKey">The target key to compare with.</param> /// <param name="targetKey">The target key to compare with.</param>
public StateKeyFilter(IStateKeyResolver<TKey> keyResolver, TKey targetKey) public StateKeyFilter(TValue? targetKey)
{ {
KeyResolver = keyResolver;
TargetKey = targetKey; TargetKey = targetKey;
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool CanPass(FilterExecutionContext<Update> context) public override bool CanPass(FilterExecutionContext<Update> context)
=> KeyResolver.ResolveKey(context.Input).Equals(TargetKey); {
string? key = new TKey().ResolveKey(context.Input);
if (key is null)
return TargetKey is null;
TValue? value = context.UpdateRouter.StateStorage.GetAsync<TValue>(key).Result;
if (value is null)
return TargetKey is null;
if (TargetKey is null)
return false;
return TargetKey.Equals(value);
}
} }
} }
@@ -1,11 +1,11 @@
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using Telegrator.Filters;
using Telegrator.StateKeeping;
using Telegrator.Core; using Telegrator.Core;
using Telegrator.Core.Handlers.Building;
using Telegrator.Core.Descriptors; using Telegrator.Core.Descriptors;
using Telegrator.Core.StateKeeping; using Telegrator.Core.Handlers.Building;
using Telegrator.Core.States;
using Telegrator.Filters;
using Telegrator.States;
namespace Telegrator.Handlers.Building namespace Telegrator.Handlers.Building
{ {
@@ -56,9 +56,21 @@ namespace Telegrator.Handlers.Building
/// <param name="keyResolver">The state key resolver to use for filtering updates.</param> /// <param name="keyResolver">The state key resolver to use for filtering updates.</param>
/// <param name="cancellationToken">The cancellation token to cancel the wait operation.</param> /// <param name="cancellationToken">The cancellation token to cancel the wait operation.</param>
/// <returns>The awaited update of type TUpdate.</returns> /// <returns>The awaited update of type TUpdate.</returns>
public async Task<TUpdate> Await(IStateKeyResolver<long> keyResolver, CancellationToken cancellationToken = default) public async Task<TUpdate> Await(IStateKeyResolver keyResolver, CancellationToken cancellationToken = default)
{ {
Filters.Add(new StateKeyFilter<long>(keyResolver, keyResolver.ResolveKey(HandlingUpdate))); string? handlingKey = keyResolver.ResolveKey(HandlingUpdate);
if (handlingKey is null)
throw new InvalidOperationException("Cannot await update with resolved key as NULL");
Filters.Add(Filter<Update>.If(ctx =>
{
string? key = keyResolver.ResolveKey(ctx.Update);
if (key is null)
return false;
return key == handlingKey;
}));
AwaiterHandler handlerInstance = new AwaiterHandler(UpdateType); AwaiterHandler handlerInstance = new AwaiterHandler(UpdateType);
HandlerDescriptor descriptor = BuildImplicitDescriptor(handlerInstance); HandlerDescriptor descriptor = BuildImplicitDescriptor(handlerInstance);
@@ -1,9 +1,7 @@
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegrator.Annotations.StateKeeping;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.Handlers.Building; using Telegrator.Core.Handlers.Building;
using Telegrator.Core.StateKeeping; using Telegrator.Core.States;
using Telegrator.StateKeeping;
namespace Telegrator.Handlers.Building namespace Telegrator.Handlers.Building
{ {
@@ -61,25 +59,13 @@ namespace Telegrator.Handlers.Building
return handlerBuilder; return handlerBuilder;
} }
/// <inheritdoc cref="HandlerBuilderBase.SetStateKeeper{TKey, TState, TKeeper}(TState, IStateKeyResolver{TKey})"/> /// <inheritdoc cref="HandlerBuilderBase.SetState{TKey, TValue}(TValue?)"/>
public static TBuilder SetStateKeeper<TBuilder, TKey, TState, TKeeper>(this TBuilder handlerBuilder, TState myState, IStateKeyResolver<TKey> keyResolver) public static TBuilder SetState<TBuilder, TKey, TValue>(this TBuilder handlerBuilder, TValue? myState)
where TBuilder : HandlerBuilderBase where TBuilder : HandlerBuilderBase
where TKey : notnull where TKey : IStateKeyResolver, new()
where TState : IEquatable<TState> where TValue : IEquatable<TValue>
where TKeeper : StateKeeperBase<TKey, TState>, new()
{ {
handlerBuilder.SetStateKeeper<TKey, TState, TKeeper>(myState, keyResolver); handlerBuilder.SetState<TKey, TValue>(myState);
return handlerBuilder;
}
/// <inheritdoc cref="HandlerBuilderBase.SetStateKeeper{TKey, TState, TKeeper}(SpecialState, IStateKeyResolver{TKey})"/>
public static TBuilder SetStateKeeper<TBuilder, TKey, TState, TKeeper>(this TBuilder handlerBuilder, SpecialState specialState, IStateKeyResolver<TKey> keyResolver)
where TBuilder : HandlerBuilderBase
where TKey : notnull
where TState : IEquatable<TState>
where TKeeper : StateKeeperBase<TKey, TState>, new()
{
handlerBuilder.SetStateKeeper<TKey, TState, TKeeper>(specialState, keyResolver);
return handlerBuilder; return handlerBuilder;
} }
@@ -116,129 +102,5 @@ namespace Telegrator.Handlers.Building
handlerBuilder.AddTargetedFilters(getFilterringTarget, filters); handlerBuilder.AddTargetedFilters(getFilterringTarget, filters);
return handlerBuilder; return handlerBuilder;
} }
/// <summary>
/// Sets a numeric state keeper with a custom key resolver.
/// </summary>
/// <typeparam name="TBuilder">The type of the handler builder.</typeparam>
/// <param name="handlerBuilder">The handler builder.</param>
/// <param name="myState">The numeric state value.</param>
/// <param name="keyResolver">The key resolver for the state.</param>
/// <returns>The handler builder for method chaining.</returns>
public static TBuilder SetNumericState<TBuilder>(this TBuilder handlerBuilder, int myState, IStateKeyResolver<long> keyResolver)
where TBuilder : HandlerBuilderBase
{
handlerBuilder.SetStateKeeper<long, int, NumericStateKeeper>(myState, keyResolver);
return handlerBuilder;
}
/// <summary>
/// Sets a numeric state keeper with a special state and custom key resolver.
/// </summary>
/// <typeparam name="TBuilder">The type of the handler builder.</typeparam>
/// <param name="handlerBuilder">The handler builder.</param>
/// <param name="specialState">The special state value.</param>
/// <param name="keyResolver">The key resolver for the state.</param>
/// <returns>The handler builder for method chaining.</returns>
public static TBuilder SetNumericState<TBuilder>(this TBuilder handlerBuilder, SpecialState specialState, IStateKeyResolver<long> keyResolver)
where TBuilder : HandlerBuilderBase
{
handlerBuilder.SetStateKeeper<long, int, NumericStateKeeper>(specialState, keyResolver);
return handlerBuilder;
}
/// <summary>
/// Sets a numeric state keeper with the default sender ID resolver.
/// </summary>
/// <typeparam name="TBuilder">The type of the handler builder.</typeparam>
/// <param name="handlerBuilder">The handler builder.</param>
/// <param name="myState">The numeric state value.</param>
/// <returns>The handler builder for method chaining.</returns>
public static TBuilder SetNumericState<TBuilder>(this TBuilder handlerBuilder, int myState)
where TBuilder : HandlerBuilderBase
{
handlerBuilder.SetStateKeeper<long, int, NumericStateKeeper>(myState, new SenderIdResolver());
return handlerBuilder;
}
/// <summary>
/// Sets a numeric state keeper with a special state and the default sender ID resolver.
/// </summary>
/// <typeparam name="TBuilder">The type of the handler builder.</typeparam>
/// <param name="handlerBuilder">The handler builder.</param>
/// <param name="specialState">The special state value.</param>
/// <returns>The handler builder for method chaining.</returns>
public static TBuilder SetNumericState<TBuilder>(this TBuilder handlerBuilder, SpecialState specialState)
where TBuilder : HandlerBuilderBase
{
handlerBuilder.SetStateKeeper<long, int, NumericStateKeeper>(specialState, new SenderIdResolver());
return handlerBuilder;
}
/// <summary>
/// Sets an enum state keeper with a custom key resolver.
/// </summary>
/// <typeparam name="TBuilder">The type of the handler builder.</typeparam>
/// <typeparam name="TEnum">The type of the enum state.</typeparam>
/// <param name="handlerBuilder">The handler builder.</param>
/// <param name="myState">The enum state value.</param>
/// <param name="keyResolver">The key resolver for the state.</param>
/// <returns>The handler builder for method chaining.</returns>
public static TBuilder SetEnumState<TBuilder, TEnum>(this TBuilder handlerBuilder, TEnum myState, IStateKeyResolver<long> keyResolver)
where TBuilder : HandlerBuilderBase
where TEnum : Enum, IEquatable<TEnum>
{
handlerBuilder.SetStateKeeper<long, TEnum, EnumStateKeeper<TEnum>>(myState, keyResolver);
return handlerBuilder;
}
/// <summary>
/// Sets an enum state keeper with a special state and custom key resolver.
/// </summary>
/// <typeparam name="TBuilder">The type of the handler builder.</typeparam>
/// <typeparam name="TEnum">The type of the enum state.</typeparam>
/// <param name="handlerBuilder">The handler builder.</param>
/// <param name="specialState">The special state value.</param>
/// <param name="keyResolver">The key resolver for the state.</param>
/// <returns>The handler builder for method chaining.</returns>
public static TBuilder SetEnumState<TBuilder, TEnum>(this TBuilder handlerBuilder, SpecialState specialState, IStateKeyResolver<long> keyResolver)
where TBuilder : HandlerBuilderBase
where TEnum : Enum, IEquatable<TEnum>
{
handlerBuilder.SetStateKeeper<long, TEnum, EnumStateKeeper<TEnum>>(specialState, keyResolver);
return handlerBuilder;
}
/// <summary>
/// Sets an enum state keeper with the default sender ID resolver.
/// </summary>
/// <typeparam name="TBuilder">The type of the handler builder.</typeparam>
/// <typeparam name="TEnum">The type of the enum state.</typeparam>
/// <param name="handlerBuilder">The handler builder.</param>
/// <param name="myState">The enum state value.</param>
/// <returns>The handler builder for method chaining.</returns>
public static TBuilder SetEnumState<TBuilder, TEnum>(this TBuilder handlerBuilder, TEnum myState)
where TBuilder : HandlerBuilderBase
where TEnum : Enum, IEquatable<TEnum>
{
handlerBuilder.SetStateKeeper<long, TEnum, EnumStateKeeper<TEnum>>(myState, new SenderIdResolver());
return handlerBuilder;
}
/// <summary>
/// Sets an enum state keeper with a special state and the default sender ID resolver.
/// </summary>
/// <typeparam name="TBuilder">The type of the handler builder.</typeparam>
/// <typeparam name="TEnum">The type of the enum state.</typeparam>
/// <param name="handlerBuilder">The handler builder.</param>
/// <param name="specialState">The special state value.</param>
/// <returns>The handler builder for method chaining.</returns>
public static TBuilder SetEnumState<TBuilder, TEnum>(this TBuilder handlerBuilder, SpecialState specialState)
where TBuilder : HandlerBuilderBase
where TEnum : Enum, IEquatable<TEnum>
{
handlerBuilder.SetStateKeeper<long, TEnum, EnumStateKeeper<TEnum>>(specialState, new SenderIdResolver());
return handlerBuilder;
}
} }
} }
@@ -43,6 +43,9 @@ namespace Telegrator.Handlers
{ {
string[] split = ReceivedCommand.Split('@'); string[] split = ReceivedCommand.Split('@');
ReceivedCommand = split[0]; ReceivedCommand = split[0];
if (!split.ElementAtOrDefault(1).Equals(context.BotInfo.User.Username))
return false;
} }
return true; return true;
+12 -5
View File
@@ -3,6 +3,7 @@ using Telegram.Bot.Types;
using Telegrator.Core; using Telegrator.Core;
using Telegrator.Core.Descriptors; using Telegrator.Core.Descriptors;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.States;
namespace Telegrator.Handlers namespace Telegrator.Handlers
{ {
@@ -33,6 +34,9 @@ namespace Telegrator.Handlers
/// <inheritdoc/> /// <inheritdoc/>
public IAwaitingProvider AwaitingProvider { get; } public IAwaitingProvider AwaitingProvider { get; }
/// <inheritdoc/>
public IStateStorage StateStorage { get; }
/// <summary> /// <summary>
/// Initializes new instance of <see cref="HandlerContainer{TUpdate}"/> /// Initializes new instance of <see cref="HandlerContainer{TUpdate}"/>
/// </summary> /// </summary>
@@ -45,6 +49,7 @@ namespace Telegrator.Handlers
ExtraData = handlerInfo.ExtraData; ExtraData = handlerInfo.ExtraData;
CompletedFilters = handlerInfo.CompletedFilters; CompletedFilters = handlerInfo.CompletedFilters;
AwaitingProvider = handlerInfo.AwaitingProvider; AwaitingProvider = handlerInfo.AwaitingProvider;
StateStorage = handlerInfo.StateStorage;
} }
/// <summary> /// <summary>
@@ -56,7 +61,8 @@ namespace Telegrator.Handlers
/// <param name="extraData"></param> /// <param name="extraData"></param>
/// <param name="filters"></param> /// <param name="filters"></param>
/// <param name="awaitingProvider"></param> /// <param name="awaitingProvider"></param>
public HandlerContainer(TUpdate actualUpdate, Update handlingUpdate, ITelegramBotClient client, Dictionary<string, object> extraData, CompletedFiltersList filters, IAwaitingProvider awaitingProvider) /// <param name="stateStorage"></param>
public HandlerContainer(TUpdate actualUpdate, Update handlingUpdate, ITelegramBotClient client, Dictionary<string, object> extraData, CompletedFiltersList filters, IAwaitingProvider awaitingProvider, IStateStorage stateStorage)
{ {
ActualUpdate = actualUpdate; ActualUpdate = actualUpdate;
HandlingUpdate = handlingUpdate; HandlingUpdate = handlingUpdate;
@@ -64,6 +70,7 @@ namespace Telegrator.Handlers
ExtraData = extraData; ExtraData = extraData;
CompletedFilters = filters; CompletedFilters = filters;
AwaitingProvider = awaitingProvider; AwaitingProvider = awaitingProvider;
StateStorage = stateStorage;
} }
/// <summary> /// <summary>
@@ -75,8 +82,8 @@ namespace Telegrator.Handlers
{ {
return new HandlerContainer<QUpdate>( return new HandlerContainer<QUpdate>(
HandlingUpdate.GetActualUpdateObject<QUpdate>(), HandlingUpdate.GetActualUpdateObject<QUpdate>(),
HandlingUpdate, Client, ExtraData, HandlingUpdate, Client, ExtraData, CompletedFilters,
CompletedFilters, AwaitingProvider); AwaitingProvider, StateStorage);
} }
/// <summary> /// <summary>
@@ -89,8 +96,8 @@ namespace Telegrator.Handlers
{ {
return new HandlerContainer<TUpdate>( return new HandlerContainer<TUpdate>(
other.HandlingUpdate.GetActualUpdateObject<TUpdate>(), other.HandlingUpdate.GetActualUpdateObject<TUpdate>(),
other.HandlingUpdate, other.Client, other.ExtraData, other.HandlingUpdate, other.Client, other.ExtraData, other.CompletedFilters,
other.CompletedFilters, other.AwaitingProvider); other.AwaitingProvider, other.StateStorage);
} }
} }
} }
@@ -28,7 +28,7 @@ namespace Telegrator.Mediation
{ {
_handler.Invoke(botClient, exception, source, cancellationToken); _handler.Invoke(botClient, exception, source, cancellationToken);
} }
finally catch
{ {
_ = 0xBAD + 0xC0DE; _ = 0xBAD + 0xC0DE;
} }
+10 -3
View File
@@ -7,6 +7,7 @@ using Telegrator.Core;
using Telegrator.Core.Descriptors; using Telegrator.Core.Descriptors;
using Telegrator.Core.Filters; using Telegrator.Core.Filters;
using Telegrator.Core.Handlers; using Telegrator.Core.Handlers;
using Telegrator.Core.States;
using Telegrator.Handlers.Diagnostics; using Telegrator.Handlers.Diagnostics;
using Telegrator.Logging; using Telegrator.Logging;
@@ -21,6 +22,7 @@ namespace Telegrator.Mediation
private readonly TelegratorOptions _options; private readonly TelegratorOptions _options;
private readonly IHandlersProvider _handlersProvider; private readonly IHandlersProvider _handlersProvider;
private readonly IAwaitingProvider _awaitingProvider; private readonly IAwaitingProvider _awaitingProvider;
private readonly IStateStorage _stateStorage;
private readonly IUpdateHandlersPool _HandlersPool; private readonly IUpdateHandlersPool _HandlersPool;
private readonly ITelegramBotInfo _botInfo; private readonly ITelegramBotInfo _botInfo;
@@ -30,6 +32,9 @@ namespace Telegrator.Mediation
/// <inheritdoc/> /// <inheritdoc/>
public IAwaitingProvider AwaitingProvider => _awaitingProvider; public IAwaitingProvider AwaitingProvider => _awaitingProvider;
/// <inheritdoc/>
public IStateStorage StateStorage => _stateStorage;
/// <inheritdoc/> /// <inheritdoc/>
public TelegratorOptions Options => _options; public TelegratorOptions Options => _options;
@@ -47,13 +52,15 @@ namespace Telegrator.Mediation
/// </summary> /// </summary>
/// <param name="handlersProvider">The provider for regular handlers.</param> /// <param name="handlersProvider">The provider for regular handlers.</param>
/// <param name="awaitingProvider">The provider for awaiting handlers.</param> /// <param name="awaitingProvider">The provider for awaiting handlers.</param>
/// <param name="stateStorage">The state storage.</param>
/// <param name="options">The bot configuration options.</param> /// <param name="options">The bot configuration options.</param>
/// <param name="botInfo"></param> /// <param name="botInfo"></param>
public UpdateRouter(IHandlersProvider handlersProvider, IAwaitingProvider awaitingProvider, TelegratorOptions options, ITelegramBotInfo botInfo) public UpdateRouter(IHandlersProvider handlersProvider, IAwaitingProvider awaitingProvider, IStateStorage stateStorage, TelegratorOptions options, ITelegramBotInfo botInfo)
{ {
_options = options; _options = options;
_handlersProvider = handlersProvider; _handlersProvider = handlersProvider;
_awaitingProvider = awaitingProvider; _awaitingProvider = awaitingProvider;
_stateStorage = stateStorage;
_HandlersPool = new UpdateHandlersPool(this, _options, _options.GlobalCancellationToken); _HandlersPool = new UpdateHandlersPool(this, _options, _options.GlobalCancellationToken);
_botInfo = botInfo; _botInfo = botInfo;
} }
@@ -235,7 +242,7 @@ namespace Telegrator.Mediation
}; };
UpdateHandlerBase handlerInstance = provider.GetHandlerInstance(descriptor, cancellationToken); UpdateHandlerBase handlerInstance = provider.GetHandlerInstance(descriptor, cancellationToken);
FilterExecutionContext<Update> filterContext = new FilterExecutionContext<Update>(_botInfo, update, update, data, []); FilterExecutionContext<Update> filterContext = new FilterExecutionContext<Update>(this, _botInfo, update, update, data, []);
if (descriptor.Filters != null) if (descriptor.Filters != null)
{ {
@@ -254,7 +261,7 @@ namespace Telegrator.Mediation
} }
} }
return new DescribedHandlerDescriptor(descriptor, this, AwaitingProvider, client, handlerInstance, filterContext, descriptor.DisplayString); return new DescribedHandlerDescriptor(descriptor, this, AwaitingProvider, StateStorage, client, handlerInstance, filterContext, descriptor.DisplayString);
} }
/// <summary> /// <summary>
@@ -1,59 +0,0 @@
using Telegrator.Core.StateKeeping;
namespace Telegrator.StateKeeping
{
/// <summary>
/// Abstract base class for state keepers that manage state transitions using an array of predefined states.
/// Provides forward and backward navigation through a fixed sequence of states.
/// </summary>
/// <typeparam name="TKey">The type of the key used to identify state contexts.</typeparam>
/// <typeparam name="TState">The type of the state values. Must be non-null.</typeparam>
/// <param name="states">The array of states that define the allowed state sequence.</param>
public abstract class ArrayStateKeeper<TKey, TState>(params TState[] states) : StateKeeperBase<TKey, TState> where TState : notnull where TKey : notnull
{
/// <summary>
/// The array of states that defines the allowed state sequence for navigation.
/// </summary>
protected readonly TState[] ArrayStates = states;
/// <summary>
/// Moves to the previous state in the array sequence.
/// </summary>
/// <param name="currentState">The current state to move backward from.</param>
/// <param name="_">The key parameter (unused in this implementation).</param>
/// <returns>The previous state in the array sequence.</returns>
/// <exception cref="ArgumentException">Thrown when the current state is not found in the array.</exception>
/// <exception cref="IndexOutOfRangeException">Thrown when trying to move backward from the first state.</exception>
protected override TState MoveBackward(TState currentState, TKey _)
{
int index = Array.IndexOf(ArrayStates, currentState);
if (index == -1)
throw new ArgumentException("Cannot resolve current state");
if (index == 0)
throw new IndexOutOfRangeException("This state cannot be moved backward");
return ArrayStates[index - 1];
}
/// <summary>
/// Moves to the next state in the array sequence.
/// </summary>
/// <param name="currentState">The current state to move forward from.</param>
/// <param name="_">The key parameter (unused in this implementation).</param>
/// <returns>The next state in the array sequence.</returns>
/// <exception cref="ArgumentException">Thrown when the current state is not found in the array.</exception>
/// <exception cref="IndexOutOfRangeException">Thrown when trying to move forward from the last state.</exception>
protected override TState MoveForward(TState currentState, TKey _)
{
int index = Array.IndexOf(ArrayStates, currentState);
if (index == -1)
throw new ArgumentException("Cannot resolve current state");
if (index == ArrayStates.Length - 1)
throw new IndexOutOfRangeException("This state cannot be moved forward");
return ArrayStates[index + 1];
}
}
}
@@ -1,75 +0,0 @@
using Telegrator.Annotations.StateKeeping;
using Telegrator.Core.Handlers;
namespace Telegrator.StateKeeping
{
/// <summary>
/// State keeper implementation for enum-based states.
/// Automatically creates an array of all enum values for state navigation.
/// </summary>
/// <typeparam name="TEnum">The enum type to be used for state management.</typeparam>
public class EnumStateKeeper<TEnum>() : ArrayStateKeeper<long, TEnum>(Enum.GetValues(typeof(TEnum)).Cast<TEnum>().ToArray()) where TEnum : Enum
{
/// <summary>
/// Gets the default state, which is the first value in the enum.
/// </summary>
public override TEnum DefaultState => ArrayStates.ElementAt(0);
}
/// <summary>
/// Extension methods for working with enum-based states in handler containers.
/// Provides convenient methods for state management operations.
/// </summary>
public static partial class StateHandlerContainerExtensions
{
/// <summary>
/// Gets the enum state keeper for the specified enum type.
/// </summary>
/// <typeparam name="TEnum">The enum type to get the state keeper for.</typeparam>
/// <param name="_">The handler container (unused parameter for extension method syntax).</param>
/// <returns>The enum state keeper instance.</returns>
public static EnumStateKeeper<TEnum> EnumStateKeeper<TEnum>(this IHandlerContainer _) where TEnum : Enum
=> EnumStateAttribute<TEnum>.Shared;
/// <summary>
/// Creates a new enum state for the current update.
/// </summary>
/// <typeparam name="TEnum">The enum type for state management.</typeparam>
/// <param name="container">The handler container.</param>
public static void CreateEnumState<TEnum>(this IHandlerContainer container) where TEnum : Enum
=> container.EnumStateKeeper<TEnum>().CreateState(container.HandlingUpdate);
/// <summary>
/// Deletes the enum state for the current update.
/// </summary>
/// <typeparam name="TEnum">The enum type for state management.</typeparam>
/// <param name="container">The handler container.</param>
public static void DeleteEnumState<TEnum>(this IHandlerContainer container) where TEnum : Enum
=> container.EnumStateKeeper<TEnum>().DeleteState(container.HandlingUpdate);
/// <summary>
/// Sets the enum state to a specific value for the current update.
/// </summary>
/// <typeparam name="TEnum">The enum type for state management.</typeparam>
/// <param name="container">The handler container.</param>
/// <param name="newState">The new state value. If null, uses the default state.</param>
public static void SetEnumState<TEnum>(this IHandlerContainer container, TEnum? newState) where TEnum : Enum
=> container.EnumStateKeeper<TEnum>().SetState(container.HandlingUpdate, newState ?? EnumStateAttribute<TEnum>.DefaultState);
/// <summary>
/// Moves the enum state forward to the next value in the enum sequence.
/// </summary>
/// <typeparam name="TEnum">The enum type for state management.</typeparam>
/// <param name="container">The handler container.</param>
public static void ForwardEnumState<TEnum>(this IHandlerContainer container) where TEnum : Enum
=> container.EnumStateKeeper<TEnum>().MoveForward(container.HandlingUpdate);
/// <summary>
/// Moves the enum state backward to the previous value in the enum sequence.
/// </summary>
/// <typeparam name="TEnum">The enum type for state management.</typeparam>
/// <param name="container">The handler container.</param>
public static void BackwardEnumState<TEnum>(this IHandlerContainer container) where TEnum : Enum
=> container.EnumStateKeeper<TEnum>().MoveBackward(container.HandlingUpdate);
}
}
@@ -1,92 +0,0 @@
using Telegrator.Annotations.StateKeeping;
using Telegrator.Core.Handlers;
using Telegrator.Core.StateKeeping;
namespace Telegrator.StateKeeping
{
/// <summary>
/// State keeper that manages numeric (integer) states for chat sessions.
/// Inherits from <see cref="StateKeeperBase{TKey, TState}"/> with long keys and int states.
/// Provides automatic increment/decrement functionality for state transitions.
/// </summary>
public class NumericStateKeeper : StateKeeperBase<long, int>
{
/// <summary>
/// Gets the default state value, which is 1.
/// </summary>
public override int DefaultState => 1;
/// <summary>
/// Moves the numeric state backward by decrementing the current state value.
/// </summary>
/// <param name="currentState">The current numeric state value</param>
/// <param name="_">The chat ID (unused in this implementation)</param>
/// <returns>The decremented state value</returns>
protected override int MoveBackward(int currentState, long _)
{
return currentState - 1;
}
/// <summary>
/// Moves the numeric state forward by incrementing the current state value.
/// </summary>
/// <param name="currentState">The current numeric state value</param>
/// <param name="_">The chat ID (unused in this implementation)</param>
/// <returns>The incremented state value</returns>
protected override int MoveForward(int currentState, long _)
{
return currentState + 1;
}
}
/// <summary>
/// Provides extension methods for managing numeric states in handler containers.
/// </summary>
public static partial class StateHandlerContainerExtensions
{
/// <summary>
/// Gets the numeric state keeper instance associated with the handler container.
/// </summary>
/// <param name="_">The handler container instance</param>
/// <returns>The <see cref="NumericStateKeeper"/> instance</returns>
public static NumericStateKeeper NumericStateKeeper(this IHandlerContainer _)
=> NumericStateAttribute.Shared;
/// <summary>
/// Creates a new numeric state for the current update being handled.
/// </summary>
/// <param name="container">The handler container instance</param>
public static void CreateNumericState(this IHandlerContainer container)
=> container.NumericStateKeeper().CreateState(container.HandlingUpdate);
/// <summary>
/// Deletes the numeric state for the current update being handled.
/// </summary>
/// <param name="container">The handler container instance</param>
public static void DeleteNumericState(this IHandlerContainer container)
=> container.NumericStateKeeper().DeleteState(container.HandlingUpdate);
/// <summary>
/// Sets the numeric state for the current update being handled.
/// If the new state is null, uses the default state from the state keeper.
/// </summary>
/// <param name="container">The handler container instance</param>
/// <param name="newState">The new numeric state to set, or null to use default</param>
public static void SetNumericState(this IHandlerContainer container, int? newState)
=> container.NumericStateKeeper().SetState(container.HandlingUpdate, newState ?? NumericStateAttribute.DefaultState);
/// <summary>
/// Moves the numeric state forward by incrementing the current value.
/// </summary>
/// <param name="container">The handler container instance</param>
public static void ForwardNumericState(this IHandlerContainer container)
=> container.NumericStateKeeper().MoveForward(container.HandlingUpdate);
/// <summary>
/// Moves the numeric state backward by decrementing the current value.
/// </summary>
/// <param name="container">The handler container instance</param>
public static void BackwardNumericState(this IHandlerContainer container)
=> container.NumericStateKeeper().MoveBackward(container.HandlingUpdate);
}
}
@@ -1,21 +0,0 @@
using Telegram.Bot.Types;
using Telegrator.Core.StateKeeping;
namespace Telegrator.StateKeeping
{
/// <summary>
/// Resolves sender ID from Telegram updates for state management purposes.
/// Extracts the sender identifier from various types of updates to provide a consistent key for state operations.
/// </summary>
public class SenderIdResolver : IStateKeyResolver<long>
{
/// <summary>
/// Resolves the sender ID from a Telegram update.
/// </summary>
/// <param name="keySource">The Telegram update to extract the sender ID from.</param>
/// <returns>The sender ID as a long value.</returns>
/// <exception cref="ArgumentException">Thrown when the update does not contain a valid sender ID.</exception>
public long ResolveKey(Update keySource)
=> keySource.GetSenderId() ?? throw new ArgumentException("Cannot resolve SenderID for this Update");
}
}
@@ -1,87 +0,0 @@
using Telegrator.Annotations.StateKeeping;
using Telegrator.Core.Handlers;
using Telegrator.Core.StateKeeping;
namespace Telegrator.StateKeeping
{
/// <summary>
/// State keeper that manages string-based states for chat sessions.
/// </summary>
public class StringStateKeeper() : StateKeeperBase<long, string>()
{
/// <summary>
/// Gets the default state value, which is an empty string.
/// </summary>
public override string DefaultState => string.Empty;
/// <inheritdoc/>
protected override string MoveBackward(string currentState, long currentKey)
{
throw new NotImplementedException();
}
/// <inheritdoc/>
protected override string MoveForward(string currentState, long currentKey)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Provides extension methods for managing string states in handler containers.
/// </summary>
public static partial class StateHandlerContainerExtensions
{
/// <summary>
/// Gets the string state keeper instance associated with the handler container.
/// </summary>
/// <param name="_">The handler container instance</param>
/// <returns>The <see cref="StringStateKeeper"/> instance</returns>
public static StringStateKeeper StringStateKeeper(this IHandlerContainer _)
=> StringStateAttribute.Shared;
/// <summary>
/// Creates a new string state for the current update being handled.
/// </summary>
/// <param name="container">The handler container instance</param>
public static void CreateStringState(this IHandlerContainer container)
=> container.StringStateKeeper().CreateState(container.HandlingUpdate);
/// <summary>
/// Deletes the string state for the current update being handled.
/// </summary>
/// <param name="container">The handler container instance</param>
public static void DeleteStringState(this IHandlerContainer container)
=> container.StringStateKeeper().DeleteState(container.HandlingUpdate);
/// <summary>
/// Sets the string state for the current update being handled.
/// If the new state is null, uses the default state from the state keeper.
/// </summary>
/// <param name="container">The handler container instance</param>
/// <param name="newState">The new string state to set, or null to use default</param>
public static void SetStringState(this IHandlerContainer container, string? newState)
=> container.StringStateKeeper().SetState(container.HandlingUpdate, newState ?? StringStateAttribute.DefaultState);
/*
public static string GetStringState(this IHandlerContainer container, string key)
=> container.StringStateKeeper().GetState()
*/
/*
/// <summary>
/// Moves the string state forward to the next state in the sequence.
/// </summary>
/// <param name="container">The handler container instance</param>
public static void ForwardStringState(this IHandlerContainer container)
=> container.StringStateKeeper().MoveForward(container.HandlingUpdate);
/// <summary>
/// Moves the string state backward to the previous state in the sequence.
/// </summary>
/// <param name="container">The handler container instance</param>
public static void BackwardStringState(this IHandlerContainer container)
=> container.StringStateKeeper().MoveBackward(container.HandlingUpdate);
*/
}
}
@@ -1,13 +1,13 @@
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegrator.Core.StateKeeping; using Telegrator.Core.States;
namespace Telegrator.StateKeeping namespace Telegrator.States
{ {
/// <summary> /// <summary>
/// Resolves chat ID from Telegram updates for state management purposes. /// Resolves chat ID from Telegram updates for state management purposes.
/// Extracts the chat identifier from various types of updates to provide a consistent key for state operations. /// Extracts the chat identifier from various types of updates to provide a consistent key for state operations.
/// </summary> /// </summary>
public class ChatIdResolver : IStateKeyResolver<long> public class ChatIdResolver : IStateKeyResolver
{ {
/// <summary> /// <summary>
/// Resolves the chat ID from a Telegram update. /// Resolves the chat ID from a Telegram update.
@@ -15,7 +15,7 @@ namespace Telegrator.StateKeeping
/// <param name="keySource">The Telegram update to extract the chat ID from.</param> /// <param name="keySource">The Telegram update to extract the chat ID from.</param>
/// <returns>The chat ID as a long value.</returns> /// <returns>The chat ID as a long value.</returns>
/// <exception cref="ArgumentException">Thrown when the update does not contain a valid chat ID.</exception> /// <exception cref="ArgumentException">Thrown when the update does not contain a valid chat ID.</exception>
public long ResolveKey(Update keySource) public string? ResolveKey(Update keySource)
=> keySource.GetChatId() ?? throw new ArgumentException("Cannot resolve ChatID for this Update"); => keySource.GetChatId()?.ToString() ?? throw new ArgumentException("Cannot resolve ChatID for this Update");
} }
} }
@@ -0,0 +1,49 @@
using System.Collections.Concurrent;
using Telegrator.Core.States;
namespace Telegrator.States;
/// <summary>
/// Defines default in-memory state storage
/// </summary>
public class DefaultStateStorage : IStateStorage
{
private readonly ConcurrentDictionary<string, object> storage = [];
/// <inheritdoc/>
public Task DeleteAsync(string key, CancellationToken cancellationToken = default)
{
if (key is null)
throw new ArgumentNullException(nameof(key));
if (!storage.TryRemove(key, out object value))
throw new Exception("Failed to remove key '" + key + "' from storage.");
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task<T?> GetAsync<T>(string key, CancellationToken ccancellationTokent = default)
{
if (key is null)
throw new ArgumentNullException(nameof(key));
if (storage.TryGetValue(key, out object value) && value is T)
return Task.FromResult((T?)value);
return Task.FromResult(default(T));
}
/// <inheritdoc/>
public Task SetAsync<T>(string key, T state, CancellationToken cancellationToken = default)
{
if (key is null)
throw new ArgumentNullException(nameof(key));
if (state is null)
throw new ArgumentNullException(nameof(state));
storage.Set(key, state);
return Task.CompletedTask;
}
}
+62
View File
@@ -0,0 +1,62 @@
using Telegrator.Core.States;
namespace Telegrator.States;
/// <summary>
/// State machine implementation for enum-based states.
/// Automatically creates an array of all enum values for state navigation.
/// </summary>
/// <typeparam name="TEnum">The enum type to be used for state management.</typeparam>
public class EnumStateMachine<TEnum> : IStateMachine<TEnum> where TEnum : struct, Enum, IEquatable<TEnum>
{
private readonly TEnum[] _states = Enum.GetValues(typeof(TEnum)).Cast<TEnum>().ToArray();
private TEnum _defaultState => _states.FirstOrDefault();
/// <inheritdoc/>
public async Task<TEnum> Current(IStateStorage storage, string updateKey, CancellationToken cancellationToken = default)
{
string key = FormatKey(updateKey);
TEnum state = await storage.GetAsync<TEnum>(key, cancellationToken);
return EqualityComparer<TEnum>.Default.Equals(state, default)
? _defaultState : state;
}
/// <inheritdoc/>
public async Task Advance(IStateStorage storage, string updateKey, CancellationToken cancellationToken = default)
{
string key = FormatKey(updateKey);
TEnum currentState = await storage.GetAsync<TEnum>(key, cancellationToken);
int currentIndex = Array.IndexOf(_states, currentState);
if (currentIndex < _states.Length - 1)
{
var nextState = _states[currentIndex + 1];
await storage.SetAsync(key, nextState, cancellationToken);
}
}
/// <inheritdoc/>
public async Task Retreat(IStateStorage storage, string updateKey, CancellationToken cancellationToken = default)
{
string key = FormatKey(updateKey);
TEnum currentState = await storage.GetAsync<TEnum>(key, cancellationToken);
int currentIndex = Array.IndexOf(_states, currentState);
if (currentIndex > 0)
{
var nextState = _states[currentIndex - 1];
await storage.SetAsync(key, nextState, cancellationToken);
}
}
/// <inheritdoc/>
public async Task Reset(IStateStorage storage, string updateKey, CancellationToken cancellationToken = default)
{
string key = FormatKey(updateKey);
await storage.SetAsync(key, _defaultState, cancellationToken);
}
private static string FormatKey(string updateKey)
=> typeof(TEnum).Name + ":" + updateKey;
}
+20
View File
@@ -0,0 +1,20 @@
using Telegram.Bot.Types;
using Telegrator.Core.States;
namespace Telegrator.States;
/// <summary>
/// Resolves sender ID from Telegram updates for state management purposes.
/// Extracts the sender identifier from various types of updates to provide a consistent key for state operations.
/// </summary>
public class SenderIdResolver : IStateKeyResolver
{
/// <summary>
/// Resolves the sender ID from a Telegram update.
/// </summary>
/// <param name="keySource">The Telegram update to extract the sender ID from.</param>
/// <returns>The sender ID as a long value.</returns>
/// <exception cref="ArgumentException">Thrown when the update does not contain a valid sender ID.</exception>
public string ResolveKey(Update keySource)
=> keySource.GetSenderId()?.ToString() ?? throw new ArgumentException("Cannot resolve SenderID for this Update");
}
+71
View File
@@ -0,0 +1,71 @@
using Telegram.Bot.Types;
using Telegrator.Core.States;
namespace Telegrator.States;
/// <inheritdoc cref="IStateMachine{TState}"/>
public class StateMachine<TMachine, TState>(IStateStorage stateStorage, Update handlingUpdate)
where TMachine : IStateMachine<TState>, new()
where TState : IEquatable<TState>
{
private readonly IStateStorage _stateStorage = stateStorage;
private readonly Update _handlingUpdate = handlingUpdate;
private readonly IStateMachine<TState> _stateMachine = new TMachine();
/// <summary>
/// Chosen key resolver
/// </summary>
public IStateKeyResolver? KeyResolver;
/// <inheritdoc cref="IStateMachine{TState}.Advance(IStateStorage, string, CancellationToken)"/>
public async Task Advance(CancellationToken cancellationToken = default)
{
if (KeyResolver is null)
throw new InvalidOperationException("KeyResolver is not set.");
string? key = KeyResolver.ResolveKey(_handlingUpdate);
if (key is null)
throw new InvalidOperationException("Failed to resolve Update key");
await _stateMachine.Advance(_stateStorage, key, cancellationToken);
}
/// <inheritdoc cref="IStateMachine{TState}.Current(IStateStorage, string, CancellationToken)"/>
public async Task<TState?> Current(CancellationToken cancellationToken = default)
{
if (KeyResolver is null)
throw new InvalidOperationException("KeyResolver is not set.");
string? key = KeyResolver.ResolveKey(_handlingUpdate);
if (key is null)
throw new InvalidOperationException("Failed to resolve Update key");
return await _stateMachine.Current(_stateStorage, key, cancellationToken);
}
/// <inheritdoc cref="IStateMachine{TState}.Reset(IStateStorage, string, CancellationToken)"/>
public async Task Reset(CancellationToken cancellationToken = default)
{
if (KeyResolver is null)
throw new InvalidOperationException("KeyResolver is not set.");
string? key = KeyResolver.ResolveKey(_handlingUpdate);
if (key is null)
throw new InvalidOperationException("Failed to resolve Update key");
await _stateMachine.Reset(_stateStorage, key, cancellationToken);
}
/// <inheritdoc cref="IStateMachine{TState}.Retreat(IStateStorage, string, CancellationToken)"/>
public async Task Retreat(CancellationToken cancellationToken = default)
{
if (KeyResolver is null)
throw new InvalidOperationException("KeyResolver is not set.");
string? key = KeyResolver.ResolveKey(_handlingUpdate);
if (key is null)
throw new InvalidOperationException("Failed to resolve Update key");
await _stateMachine.Retreat(_stateStorage, key, cancellationToken);
}
}
+6 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
@@ -14,7 +14,7 @@
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<Title>Telegrator : Telegram.Bot mediator framework</Title> <Title>Telegrator : Telegram.Bot mediator framework</Title>
<Version>1.16.3</Version> <Version>1.16.4</Version>
<Authors>Rikitav Tim4ik</Authors> <Authors>Rikitav Tim4ik</Authors>
<Company>Rikitav Tim4ik</Company> <Company>Rikitav Tim4ik</Company>
<RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl> <RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl>
@@ -44,4 +44,8 @@
<ProjectReference Include="..\..\dev\Telegrator.RoslynGenerators\Telegrator.RoslynGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> <ProjectReference Include="..\..\dev\Telegrator.RoslynGenerators\Telegrator.RoslynGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Annotations\Targetted\" />
</ItemGroup>
</Project> </Project>
+3 -1
View File
@@ -4,6 +4,7 @@ using Telegrator.Core;
using Telegrator.Logging; using Telegrator.Logging;
using Telegrator.Mediation; using Telegrator.Mediation;
using Telegrator.Providers; using Telegrator.Providers;
using Telegrator.States;
namespace Telegrator namespace Telegrator
{ {
@@ -75,8 +76,9 @@ namespace Telegrator
HandlersProvider handlerProvider = new HandlersProvider(Handlers, Options); HandlersProvider handlerProvider = new HandlersProvider(Handlers, Options);
AwaitingProvider awaitingProvider = new AwaitingProvider(Options); AwaitingProvider awaitingProvider = new AwaitingProvider(Options);
DefaultStateStorage stateStorage = new DefaultStateStorage();
updateRouter = new UpdateRouter(handlerProvider, awaitingProvider, Options, BotInfo); updateRouter = new UpdateRouter(handlerProvider, awaitingProvider, stateStorage, Options, BotInfo);
// Log startup // Log startup
TelegratorLogging.LogInformation($"Telegrator bot starting up - BotId: {BotInfo.User.Id}, Username: {BotInfo.User.Username}, MaxParallelHandlers: {Options.MaximumParallelWorkingHandlers ?? -1}"); TelegratorLogging.LogInformation($"Telegrator bot starting up - BotId: {BotInfo.User.Id}, Username: {BotInfo.User.Username}, MaxParallelHandlers: {Options.MaximumParallelWorkingHandlers ?? -1}");
+94 -14
View File
@@ -3,14 +3,13 @@ using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.Payments; using Telegram.Bot.Types.Payments;
using Telegrator.Annotations; using Telegrator.Annotations;
using Telegrator.Attributes;
using Telegrator.Core; using Telegrator.Core;
using Telegrator.Core.Descriptors; using Telegrator.Core.Descriptors;
using Telegrator.Core.Handlers; using Telegrator.Core.Handlers;
using Telegrator.Core.Handlers.Building; using Telegrator.Core.Handlers.Building;
using Telegrator.Core.StateKeeping; using Telegrator.Core.States;
using Telegrator.Handlers.Building; using Telegrator.Handlers.Building;
using Telegrator.StateKeeping; using Telegrator.States;
namespace Telegrator namespace Telegrator
{ {
@@ -187,17 +186,6 @@ namespace Telegrator
/// <returns>An awaiter builder for callback query updates.</returns> /// <returns>An awaiter builder for callback query updates.</returns>
public static IAwaiterHandlerBuilder<CallbackQuery> AwaitCallbackQuery(this IHandlerContainer container) public static IAwaiterHandlerBuilder<CallbackQuery> AwaitCallbackQuery(this IHandlerContainer container)
=> container.AwaitUpdate<CallbackQuery>(UpdateType.CallbackQuery); => container.AwaitUpdate<CallbackQuery>(UpdateType.CallbackQuery);
/// <summary>
/// Gets a state keeper instance for the specified types.
/// </summary>
/// <typeparam name="TKey">The type of the state key.</typeparam>
/// <typeparam name="TState">The type of the state value.</typeparam>
/// <typeparam name="TKeeper">The type of the state keeper.</typeparam>
/// <param name="_">The handler container (unused).</param>
/// <returns>The state keeper instance.</returns>
public static TKeeper GetStateKeeper<TKey, TState, TKeeper>(this IHandlerContainer _) where TKey : notnull where TState : IEquatable<TState> where TKeeper : StateKeeperBase<TKey, TState>, new()
=> StateKeeperAttribute<TKey, TState, TKeeper>.Shared;
} }
/// <summary> /// <summary>
@@ -296,6 +284,98 @@ namespace Telegrator
} }
} }
/// <summary>
/// Provides extension methods for <see cref="IStateStorage"/> to easily initialize state machines.
/// </summary>
public static class StateStorageExtensions
{
/// <summary>
/// Initializes a state machine using the default <see cref="EnumStateMachine{TState}"/> for the specified update.
/// </summary>
/// <typeparam name="TState">The enum type representing the state.</typeparam>
/// <param name="stateStorage">The storage mechanism used to persist the state.</param>
/// <param name="handlingUpdate">The update context to resolve the state key from.</param>
/// <returns>A new instance of <see cref="StateMachine{TMachine, TState}"/>.</returns>
public static StateMachine<EnumStateMachine<TState>, TState> GetStateMachine<TState>(this IStateStorage stateStorage, Update handlingUpdate)
where TState : struct, Enum, IEquatable<TState>
=> new StateMachine<EnumStateMachine<TState>, TState>(stateStorage, handlingUpdate);
/// <summary>
/// Initializes a specific custom state machine for the specified update.
/// </summary>
/// <typeparam name="TMachine">The type of the state machine logic implementation.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam>
/// <param name="stateStorage">The storage mechanism used to persist the state.</param>
/// <param name="handlingUpdate">The update context to resolve the state key from.</param>
/// <returns>A new instance of <see cref="StateMachine{TMachine, TState}"/>.</returns>
public static StateMachine<TMachine, TState> GetStateMachine<TMachine, TState>(this IStateStorage stateStorage, Update handlingUpdate)
where TMachine : IStateMachine<TState>, new()
where TState : IEquatable<TState>
=> new StateMachine<TMachine, TState>(stateStorage, handlingUpdate);
/// <summary>
/// Initializes a state machine and explicitly configures it to resolve keys by the chat ID.
/// </summary>
/// <typeparam name="TMachine">The type of the state machine logic implementation.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam>
/// <param name="stateStorage">The storage mechanism used to persist the state.</param>
/// <param name="handlingUpdate">The update context to resolve the state key from.</param>
/// <returns>A configured instance of <see cref="StateMachine{TMachine, TState}"/>.</returns>
public static StateMachine<TMachine, TState> ByChatId<TMachine, TState>(this IStateStorage stateStorage, Update handlingUpdate)
where TMachine : IStateMachine<TState>, new()
where TState : IEquatable<TState>
=> new StateMachine<TMachine, TState>(stateStorage, handlingUpdate).ByChatId();
/// <summary>
/// Initializes a state machine and explicitly configures it to resolve keys by the sender (user) ID.
/// </summary>
/// <typeparam name="TMachine">The type of the state machine logic implementation.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam>
/// <param name="stateStorage">The storage mechanism used to persist the state.</param>
/// <param name="handlingUpdate">The update context to resolve the state key from.</param>
/// <returns>A configured instance of <see cref="StateMachine{TMachine, TState}"/>.</returns>
public static StateMachine<TMachine, TState> BySenderId<TMachine, TState>(this IStateStorage stateStorage, Update handlingUpdate)
where TMachine : IStateMachine<TState>, new()
where TState : IEquatable<TState>
=> new StateMachine<TMachine, TState>(stateStorage, handlingUpdate).BySenderId();
}
/// <summary>
/// Provides fluent extension methods for configuring <see cref="StateMachine{TMachine, TState}"/> instances.
/// </summary>
public static class StateMachineExtensions
{
/// <summary>
/// Configures the state machine to use a <see cref="ChatIdResolver"/> for state key resolution.
/// </summary>
/// <typeparam name="TMachine">The type of the state machine logic implementation.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam>
/// <param name="stateMachine">The state machine instance to configure.</param>
/// <returns>The same state machine instance for method chaining.</returns>
public static StateMachine<TMachine, TState> ByChatId<TMachine, TState>(this StateMachine<TMachine, TState> stateMachine)
where TMachine : IStateMachine<TState>, new()
where TState : IEquatable<TState>
{
stateMachine.KeyResolver = new ChatIdResolver();
return stateMachine;
}
/// <summary>
/// Configures the state machine to use a <see cref="SenderIdResolver"/> for state key resolution.
/// </summary>
/// <typeparam name="TMachine">The type of the state machine logic implementation.</typeparam>
/// <typeparam name="TState">The type of the state.</typeparam>
/// <param name="stateMachine">The state machine instance to configure.</param>
/// <returns>The same state machine instance for method chaining.</returns>
public static StateMachine<TMachine, TState> BySenderId<TMachine, TState>(this StateMachine<TMachine, TState> stateMachine)
where TMachine : IStateMachine<TState>, new()
where TState : IEquatable<TState>
{
stateMachine.KeyResolver = new SenderIdResolver();
return stateMachine;
}
}
/// <summary> /// <summary>
/// Extension methods for handlers collections. /// Extension methods for handlers collections.
/// Provides convenient methods for creating implicit handlers. /// Provides convenient methods for creating implicit handlers.
@@ -29,7 +29,7 @@ public class FilterTests
{ {
// Arrange (Given) - подготовка тестовых данных // Arrange (Given) - подготовка тестовых данных
var anyFilter = Filter<Update>.Any(); var anyFilter = Filter<Update>.Any();
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList()); var context = new FilterExecutionContext<Update>(null, new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
// Act (When) - выполнение тестируемого действия // Act (When) - выполнение тестируемого действия
var result = anyFilter.CanPass(context); var result = anyFilter.CanPass(context);
@@ -49,7 +49,7 @@ public class FilterTests
// Arrange // Arrange
var alwaysTrueFilter = Filter<Update>.Any(); var alwaysTrueFilter = Filter<Update>.Any();
var reverseFilter = alwaysTrueFilter.Not(); var reverseFilter = alwaysTrueFilter.Not();
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList()); var context = new FilterExecutionContext<Update>(null, new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
// Act // Act
var result = reverseFilter.CanPass(context); var result = reverseFilter.CanPass(context);
@@ -74,7 +74,7 @@ public class FilterTests
var firstFilter = Filter<Update>.If(_ => firstResult); var firstFilter = Filter<Update>.If(_ => firstResult);
var secondFilter = Filter<Update>.If(_ => secondResult); var secondFilter = Filter<Update>.If(_ => secondResult);
var andFilter = firstFilter.And(secondFilter); var andFilter = firstFilter.And(secondFilter);
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList()); var context = new FilterExecutionContext<Update>(null, new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
// Act // Act
var result = andFilter.CanPass(context); var result = andFilter.CanPass(context);
@@ -99,7 +99,7 @@ public class FilterTests
var firstFilter = Filter<Update>.If(_ => firstResult); var firstFilter = Filter<Update>.If(_ => firstResult);
var secondFilter = Filter<Update>.If(_ => secondResult); var secondFilter = Filter<Update>.If(_ => secondResult);
var orFilter = firstFilter.Or(secondFilter); var orFilter = firstFilter.Or(secondFilter);
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList()); var context = new FilterExecutionContext<Update>(null, new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
// Act // Act
var result = orFilter.CanPass(context); var result = orFilter.CanPass(context);
@@ -122,7 +122,7 @@ public class FilterTests
var filter3 = Filter<Update>.If(_ => false); var filter3 = Filter<Update>.If(_ => false);
var compiledFilter = new CompiledFilter<Update>(filter1, filter2, filter3); var compiledFilter = new CompiledFilter<Update>(filter1, filter2, filter3);
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList()); var context = new FilterExecutionContext<Update>(null, new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
// Act // Act
var result = compiledFilter.CanPass(context); var result = compiledFilter.CanPass(context);
@@ -164,7 +164,7 @@ public class FilterTests
wasCalled = true; wasCalled = true;
return true; return true;
}); });
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList()); var context = new FilterExecutionContext<Update>(null, new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
// Act // Act
var result = functionFilter.CanPass(context); var result = functionFilter.CanPass(context);