commit 33d1f6218a329b4a4c83edeec3bd14c752fe6056 Author: Rikitav Date: Thu Jul 24 23:19:59 2025 +0400 Добавьте файлы проекта. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0aedcec --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{cs,vb}] + +# IDE0305: Упростите инициализацию коллекции +dotnet_style_prefer_collection_expression = never diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 0000000..4815ada --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,526 @@ +# Getting Started with Telegrator + +--- + +This guide will walk you through the core concepts and features of **Telegrator**. + +- [1. Introduction](#1-introduction) +- [2. Key Concepts](#2-key-concepts) +- [3. Installation](#3-installation) +- [4. Your First Bot: A "Hello, World!" Example](#4-your-first-bot-a-hello-world-example) +- [5. Step-by-Step Tutorial](#5-step-by-step-tutorial) +- [6. Advanced Topics](#6-advanced-topics) +- [7. FAQ / Troubleshooting](#7-faq--troubleshooting) +- [8. Links](#8-links) + +--- + +## 1. Introduction + +Welcome to **Telegrator** — a modern, aspect-oriented, mediator-based framework for building powerful and maintainable Telegram bots in C#. + +- **Why Telegrator?** + - Decentralized logic: no more monolithic state machines. + - Flexible filtering and handler composition. + - Advanced state management and concurrency control. + - Inspired by AOP, but tailored for practical bot development. + +--- + +## 2. Key Concepts + +At the core of **Telegrator** are a few fundamental concepts that enable its power and flexibility. Understanding them is key to building robust and scalable bots. + +### Mediator: The Central Dispatcher +The framework is built around the **Mediator pattern**. Every incoming update from Telegram (like a message, a button click, or a user joining a chat) is first received by a central `UpdateRouter`. This router acts as a mediator, responsible for dispatching the update to the appropriate handlers. This decouples the update receiving logic from the processing logic, making the system clean and maintainable. + +### Handlers: The Logic Processors +A **Handler** is a class that contains the logic for processing a specific type of update. For example, you might have a `WelcomeHandler` for new chat members, a `CommandHandler` for slash commands, or a `CallbackQueryHandler` for button clicks. Each handler is a small, focused, and reusable component. + +* **`MessageHandler`**: For processing text messages. +* **`CallbackQueryHandler`**: For handling button clicks from `InlineKeyboardMarkup`. +* **`AnyUpdateHandler`**: A catch-all handler for any type of update. + +### Filters: The Gatekeepers +**Filters** are attributes that you apply to your handler classes to specify *when* a handler should be executed. They act as gatekeepers, ensuring that a handler only runs if the incoming update meets certain criteria. + +- **`[Command("/start")]`**: Triggers the handler only for the `/start` command. +- **`[MessageText(Contains = "hello")]`**: Triggers when a message contains the word "hello". +- **`[MessageChat(Is = ChatType.Private)]`**: Triggers only for messages from private chats. +- **`[RepliedMessage]`**: Triggers only when the message is a reply to another message. + +Filters can be combined with logical operators (`OrNext`, `Not`) to create complex and precise routing rules without writing messy `if/else` statements. + +### State Management: The Memory +**State Management** in Telegrator is a powerful feature for creating multi-step conversations (wizards, forms, quizzes) without a database. It works by using special **filter attributes** that make handlers execute only when a user or chat is in a specific state. + +The core idea is that you don't interact with a "StateKeeper" object directly. Instead: +1. You define a state machine using a C# `enum` or simple `int` values. +2. You use `[EnumState(YourEnum.SomeState)]` or `[NumericState(1)]` attributes to filter which handler should run at which step. +3. Inside a handler, you use extension methods on the `IAbstractHandlerContainer` (which I'll call `container` for simplicity) to change the user's state, e.g., `container.ForwardEnumState()`. + +#### Example: A Multi-Step Quiz + +Let's build a simple two-question quiz. + +**1. Define the Quiz States:** + +```csharp +// Enums/QuizState.cs +public enum QuizState +{ + // We use the SpecialState enum to represent the "no state" condition. + // This is the default state for all users. + Start = SpecialState.NoState, + ExpectingAnswer1, + ExpectingAnswer2 +} +``` + +**2. Build the Handlers:** + +The `/quiz` command will start the process. It will only trigger for users who are not currently in a quiz (`QuizState.Start`). + +```csharp +// Handlers/QuizHandler.cs +using Telegrator.Annotations.StateKeeping; + +[MessageHandler] +[CommandAllias("quiz")] +// This handler only runs if the user's state for QuizState is the default (NoState). +[EnumState(QuizState.Start)] +public class StartQuizHandler : MessageHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + // Create the state for the user and move it to the first question. + container.ForwardNumericState(); // If state isnt created, creates default state and formards its value. QuizState.Start -> QuizState.ExpectingAnswer1 + await Reply("Quiz started! Question 1: What is the capital of France?"); + } +} +``` + +Now, create handlers for each expected answer. They will only trigger if the user is in the correct state. + +```csharp +// Handlers/QuizAnswerHandlers.cs + +[MessageHandler] +// This handler only runs if the user's state is ExpectingAnswer1. +[EnumState(QuizState.ExpectingAnswer1)] +public class Answer1Handler : MessageHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + if (Input.Text.Trim().Equals("Paris", StringComparison.InvariantCultureIgnoreCase)) + { + await Reply("Correct!"); + } + else + { + await Reply("Incorrect. The answer is Paris."); + } + + // Move to the next state. + container.ForwardEnumState(); // Moves state to ExpectingAnswer2 + await Reply("Question 2: What is 2 + 2?"); + } +} + +[MessageHandler] +// This handler only runs if the user's state is ExpectingAnswer2. +[EnumState(QuizState.ExpectingAnswer2)] +public class Answer2Handler : MessageHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + if (Input.Text.Trim() == "4") + { + await Reply("Correct!"); + } + else + { + await Reply("Incorrect. The answer is 4."); + } + + // The quiz is over, so we delete the user's state. + container.DeleteEnumState(); + await Reply("Quiz finished!"); + } +} +``` + +**How it Works:** +- **`[EnumState(QuizState.Start)]`**: This is a **filter**. It checks the user's current state for the `QuizState` enum. `SpecialState.NoState` is a conventional way to say "the user has no state set yet". +- **`container.CreateEnumState()`**: This initializes the state for the current user (or chat, depending on the key resolver) and sets it to the first actual value of the enum (`Start`). +- **`container.ForwardEnumState()`**: This moves the user's state to the next value in the enum sequence. +- **`container.DeleteEnumState()`**: This removes the state for the user, effectively resetting them to `SpecialState.NoState`. + +This declarative, attribute-based approach keeps your handler logic clean and focused on a single task, while the framework manages the complexity of the state machine. + +--- + +## 3. Installation + +**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 +- .NET 6.0 SDK or later. +- A Telegram Bot Token from [@BotFather](https://t.me/BotFather). + +### .NET CLI + +```shell +dotnet add package Telegrator +``` + +### Package Manager Console + +```shell +Install-Package Telegrator +``` + +The framework also has integrations for different hosting models, which can be installed separately: + +- **`Telegrator.Hosting`**: For console applications and background services. +- **`Telegrator.Hosting.Web`**: For ASP.NET Core applications and Webhook support. + +```shell +# For console apps +dotnet add package Telegrator.Hosting + +# For ASP.NET Core apps +dotnet add package Telegrator.Hosting.Web +``` + +--- + +## 4. Your First Bot: A "Hello, World!" Example + +Let's create a simple bot that replies with "Hello, {FirstName}!" when a user sends the `/start` command. + +### 1. Create the Handler + +First, create a new class that inherits from `MessageHandler`. This class will contain the logic for handling the message. + +```csharp +// Handlers/StartHandler.cs +using System.Threading; +using System.Threading.Tasks; +using Telegram.Bot.Types; +using Telegrator.Handlers; +using Telegrator.Annotations; + +namespace Handlers; + +// This attribute registers the class as a message handler. +[MessageHandler] +// This filter ensures that the message's text equals to "Hello". +[TextEquals("Hello")] +public class StartHandler : MessageHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + // Get the user's first name from the incoming message. + var firstName = Input.From?.FirstName ?? "User"; + + // Reply to the user. + await Reply($"Hello, {firstName}!", cancellationToken: cancellation); + } +} +``` + +### 2. Set Up and Run the Bot + +Next, in your application's entry point (e.g., `Program.cs`), create an instance of `ReactiveClient`, add your handler, and start listening for updates. + +```csharp +// Program.cs +using System; +using System.Threading; +using Telegrator; +using Handlers; // Assuming your handler is in the "Handlers" namespace + +class Program +{ + static void Main(string[] args) + { + // Replace "" with your actual bot token. + var bot = new ReactiveClient(""); + + // Automatically discover and add all **public** handlers from the current assembly. + bot.Handlers.CollectHandlersDomainWide(); + + // Or, add a specific handler manually: + // bot.Handlers.AddHandler(); + + // Start receiving updates from Telegram. + bot.StartReceiving(); + + Console.WriteLine("Bot started. Press any key to exit."); + Console.ReadKey(); + } +} +``` + +### What's Happening? +1. **`[MessageHandler]`**: This attribute marks `StartHandler` as a handler for `Message` updates. +2. **`[TextEquals("Hello")]`**: This is a **Filter**. It tells the `UpdateRouter` to only execute this handler if the message text is equals `Hello`. +3. **`ReactiveClient`**: This is the main bot client. It manages the connection to Telegram and the update processing pipeline. +4. **`bot.Handlers.CollectHandlersDomainWide()`**: This convenient extension method scans your project for all classes marked with handler attributes (like `[MessageHandler]`) and registers them automatically. +5. **`bot.StartReceiving()`**: This method starts the long-polling loop to fetch updates from Telegram and passes them to the `UpdateRouter`. +6. **`Reply(...)`**: This is a helper method from the base `MessageHandler` class that simplifies sending a reply to the original message. + +--- + +## 5. Step-by-Step Tutorial + +This tutorial will guide you through building a bot that demonstrates common features like command handling, filtering, and waiting for user input. + +### 5.1 The Problem: An Overly Eager Echo Bot + +Let's start with two simple handlers: one for the `/start` command and one to echo messages. + +```csharp +// StartHandler.cs +[CommandHandler] +[CommandAllias("start")] +public class StartHandler : CommandHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + await Reply("Welcome! Send me any message and I will echo it back."); + } +} + +// EchoHandler.cs +[MessageHandler] +public class EchoHandler : MessageHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + await Reply($"You said: {Input.Text}"); + } +} +``` + +If you run this bot and send `/start`, you'll get two replies! +1. "Welcome! Send me any message and I will echo it back." (from `StartHandler`) +2. "You said: /start" (from `EchoHandler`) + +This happens because `EchoHandler` has no filters, so it triggers for *every* message, including the `/start` command. + +### 5.2 The Traditional Fix: `if` Statements + +The classic way to solve this is to add a check inside the `EchoHandler`. + +```csharp +// EchoHandler.cs (with an if-statement) +[MessageHandler] +public class EchoHandler : MessageHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + // Manually ignore commands. + if (Input.Text.StartsWith("/")) + { + return; + } + + await Reply($"You said: {Input.Text}"); + } +} +``` +This works, but it clutters our handler with boilerplate logic. As you add more commands and conditions, this approach becomes messy. + +### 5.3 The Reactive Fix: Declarative Filters + +**Telegrator** lets you solve this cleanly using filter attributes. We can tell the `EchoHandler` to only trigger if the message is *not* a command. + +```csharp +// EchoHandler.cs (the reactive way) +using Telegrator.Attributes; // For FilterModifier + +[MessageHandler] +// This filter ensures the handler only runs for messages that are NOT the command. +[TextStartsWith("/", Modifiers = FilterModifier.Not)] +public class EchoHandler : MessageHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + await Reply($"You said: {Input.Text}"); + } +} +``` +Now, the `EchoHandler` will correctly ignore the any command without any `if` statements in the handler body. The routing logic is declared right where it belongs: on the class itself. + +### 5.4 Waiting for Input with `AwaitingProvider` + +What if you want to ask a question and wait for the user's *next* message? You could use the state management system, but for a simple one-off question, that's overkill. The `AwaitingProvider` is perfect for this. + +Let's create a `/question` command that asks for the user's name and then greets them with the name they provide. + +```csharp +// QuestionHandler.cs +[CommandHandler] +[CommandAllias("question")] +public class QuestionHandler : CommandHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + await Reply("What is your name?"); + + // Await the user's next message. + // We apply a filter to ensure we only catch messages from the same user in the same chat. + var nextMessage = await Container.AwaitMessage().BySenderId(cancellation); + await Client.SendMessage(Input.Chat, $"Hello, {nextMessage.Text}!"); + } +} +``` + +**How it works:** +1. The handler first sends the question "What is your name?". +2. `AwaitMessage()` temporarily registers an internal handler that waits for the next `Message` sended by this user. + +This pattern is extremely powerful for creating dynamic, interactive conversations without the complexity of a full state machine. + +--- + +## 6. Advanced Topics + +This section covers more advanced features of the framework. + +### Handler Concurrency and Priority +By default, handlers are executed in the order they are added. However, you can control the execution order using the `Priority` property in the handler attribute. A Greater number means higher priority. + +```csharp +[MessageHandler(Priority = 1)] // This will run before handlers with the default priority (0) +public class HighPriorityHandler : MessageHandler +{ + // ... +} +``` + +### Creating Custom Filters +You can create your own filters by inheriting from `UpdateFilterAttribute`. This is useful for encapsulating complex or reusable filtering logic. + +Let's create a filter that only allows messages from a specific user ID. + +```csharp +// Filters/AdminOnlyFilter.cs +using Telegram.Bot.Types; +using Telegrator.Attributes; + +public class AdminOnlyAttribute : UpdateFilterAttribute +{ + private readonly long _adminId; + + public AdminOnlyAttribute(long adminId) + { + _adminId = adminId; + } + + public override Message? GetFilterringTarget(Update update) => update.Message; + + public override bool CanPass(FilterExecutionContext context) + { + return context.Input.From?.Id == _adminId; + } +} + +// Usage in a handler: +[MessageHandler] +[AdminOnly(123456789)] // Replace with your actual admin ID +public class AdminCommandHandler : MessageHandler +{ + // ... +} +``` + + +### Integration with Dependency Injection +**Telegrator** is designed to work seamlessly with dependency injection (DI) containers like the one built into ASP.NET Core. + +When using the `Telegrator.Hosting` or `Telegrator.Hosting.Web` packages, handlers and their dependencies are automatically registered with the DI container. This means you can inject any registered service directly into your handler's constructor. + +```csharp +[MessageHandler] +public class MyHandler : MessageHandler +{ + private readonly IMyService _myService; + private readonly ILogger _logger; + + // Dependencies are injected automatically. + public MyHandler(IMyService myService, ILogger logger) + { + _myService = myService; + _logger = logger; + } + + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + _logger.LogInformation("MyHandler executed!"); + var result = _myService.DoSomething(); + await Reply(result); + } +} +``` + +### Concurrency Control +You can limit the number of concurrent executions for a specific handler by setting the `concurrency` parameter in the handler attribute. This is useful for preventing race conditions or for managing resource-intensive operations. + +```csharp +// This handler will only allow one execution at a time for a given chat. +[MessageHandler(concurrency: 1)] +public class SlowHandler : MessageHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + await Task.Delay(5000, cancellation); // Simulate a long-running operation + await Reply("Done!"); + } +} +``` + +--- + +## 7. FAQ / Troubleshooting + +### Q: My handler is not being triggered. What should I do? +- **Check Handler Registration**: Ensure you are calling `bot.Handlers.AddHandlers()` or `bot.Handlers.AddHandler()`. If you are using DI, make sure the assembly containing your handlers is being scanned. +- **Check Filters**: Double-check your filter attributes. A common mistake is a typo in a command or text filter. Remember that filters are combined with a logical AND by default. If you have multiple filters, the update must pass all of them. +- **Check Update Type**: Make sure your handler is for the correct update type. A `MessageHandler` will not be triggered by a `CallbackQuery` update. +- **Enable Debug Logging**: You can subscribe to the `UpdateRouter.OnUpdate` and `UpdateRouter.OnHandlerEnter` events to see how updates are being processed in real-time. + +### Q: How can I access the `ITelegramBotClient` or the original `Update` object inside a handler? +The base `AbstractUpdateHandler` class provides access to these: +- **`Client`**: The `ITelegramBotClient` instance. +- **`Update`**: The raw `Update` object. +- **`Input`**: The specific update payload (e.g., `Message`, `CallbackQuery`). + +### Q: How do I handle errors? +You can subscribe to the `UpdateRouter.OnError` event to receive notifications about exceptions that occur during update processing. This is a good place to log errors or send notifications to an administrator. + +```csharp +bot.UpdateRouter.OnError += (sender, args) => +{ + Console.WriteLine($"An error occurred: {args.Exception.Message}"); + return Task.CompletedTask; +}; +``` + +### Q: How can I organize my code for a large bot? +- **Folders**: Organize your handlers, filters, and state keepers into separate folders (e.g., `Handlers/Commands`, `Handlers/Callbacks`, `StateKeepers`). +- **Feature Modules**: For very large bots, consider structuring your code into "feature modules", where each module is a separate class library containing all the related handlers, filters, and services for a specific feature. + +--- + +## 8. Links + +- [API Reference](./TelegramReactive_Api.md) +- [Main Repository](https://github.com/Rikitav/Telegrator) +- [Wiki & Examples](https://github.com/Rikitav/Telegrator/wiki/) + +--- + +> **Feel free to contribute, ask questions, or open issues!** \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a1a784 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# Telegram.Reactive + +![Telegram.Reactive Banner](https://github.com/Rikitav/Telegram.Reactive/blob/master/resources%2Ftgr-banner.png) + +> **A modern reactive framework for Telegram bots in C# with aspect-oriented design, mediator-based dispatching, and flexible architecture.** + +--- + +## 🚀 About Telegram.Reactive + +Telegram.Reactive is a next-generation framework for building Telegram bots in C#, inspired by AOP (Aspect-Oriented Programming) and the mediator pattern. It enables decentralized, easily extensible, and maintainable bot logic without traditional state machines or monolithic handlers. + +--- + +## ✨ Key Features + +- **Aspect-oriented approach**: Handlers and filters are "aspects" of the bot, easily composable and extendable. +- **Decentralized logic**: Each handler is an independent module—no more giant switch/case blocks. +- **Mediator-based dispatching**: All updates are routed through a powerful mediator-dispatcher. +- **Flexible filtering**: Filters for commands, text, sender, chat, regex, and much more. +- **Execution order and priorities**: Easily control handler priorities and execution order. +- **Thread safety and concurrency control**: Limit the number of concurrent handlers, await other updates inside a handler. +- **Extensibility via attributes and providers**: Easily add your own filters, handlers, and state keepers. +- **Minimal boilerplate—maximum declarativity!** + +--- + +## 🧩 Architecture & Approach + +- **Decentralization**: Bot logic is split into independent handlers (aspects), each responsible for its own part. +- **Mediator**: All Telegram updates go through a mediator, which decides which handlers should process them and in what order. +- **Filters**: Describe handler trigger conditions in a flexible, declarative way. +- **State**: Built-in mechanisms for user/chat state without manual state machines. + +--- + +## 🛠️ Quick Start + +### 1. Installation + +```shell +# Coming soon: will be available via NuGet +# dotnet add package Telegram.Reactive +``` + +### 2. Minimal Bot Example + +```csharp +using Telegram.Reactive.Handlers; +using Telegram.Reactive.Annotations; + +[MessageHandler] +public class HelloHandler : MessageHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + await Reply("Hello, world!", cancellationToken: cancellation); + } +} + +// Registration and launch: +var bot = new ReactiveClient(""); +bot.Handlers.AddHandler(); +bot.StartReceiving(); +``` + +### 3. Adding Filtering and Commands + +```csharp +using Telegram.Bot.Types.Enums; +using Telegram.Reactive.Handlers; +using Telegram.Reactive.Annotations; + +[CommandHandler, CommandAllias("start", "hello"), ChatType(ChatType.Private)] +public class StartCommandHandler : CommandHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + await Responce("Welcome!", cancellationToken: cancellation); + } +} + +// Registration: +bot.Handlers.AddHandler(); +``` + +### 4. State Management Example + +```csharp +using Telegram.Reactive.Handlers; +using Telegram.Reactive.Annotations; + +[CommandHandler, CommandAllias("first"), NumericState(SpecialState.NoState)] +public class StateKeepFirst : CommandHandler +{ + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + container.CreateNumericState(); + container.ForwardNumericState(); + await Reply("first state moved (1)", cancellationToken: cancellation); + } +} + +// Registration: +bot.Handlers.AddHandler(); +``` + +--- + +## 🏆 Why Telegram.Reactive over state machines? + +- **No tangled switch/case**—logic is split into independent handlers. +- **Flexible dispatching**—the mediator decides who and when processes an event. +- **Simple state management**—no need to implement state machines manually. +- **Easy scaling**—add new handlers without rewriting old ones. +- **High code readability and maintainability.** + +--- + +## 📚 Documentation & Examples + +- [Documentation (in progress)](https://github.com/Rikitav/Telegram.Reactive/wiki/) +- [Usage examples](https://github.com/Rikitav/Telegram.Reactive/tree/master/Examples) + +--- + +## 🤝 Contribution & Feedback + +We welcome your questions, suggestions, and pull requests! Open issues or contact us directly. + +--- + +## ⚡ License + +GPLv3 diff --git a/Telegrator.Analyzers/AnalyzerReleases.Shipped.md b/Telegrator.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..2080dcd --- /dev/null +++ b/Telegrator.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md b/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..eb6ae75 --- /dev/null +++ b/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +TR0001 | Aspect | Error | DiagnosticsHelper \ No newline at end of file diff --git a/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs b/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs new file mode 100644 index 0000000..4985dc0 --- /dev/null +++ b/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs @@ -0,0 +1,116 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Text; + +namespace Telegrator.Analyzers +{ + [Generator(LanguageNames.CSharp)] + public class DeveloperHelperAnalyzer : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValueProvider> pipeline = context.SyntaxProvider + .CreateSyntaxProvider(Provide, Transform) + .Where(handler => handler != null) + .Collect(); + + context.RegisterSourceOutput(pipeline, Execute); + } + + private static bool Provide(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (syntaxNode is not ClassDeclarationSyntax classSyntax) + return false; + + if (classSyntax.BaseList?.Types.Count == 0 && classSyntax.AttributeLists.SelectMany(list => list.Attributes).Count() == 0) + return false; + + return true; + } + + private static HandlerDeclarationModel Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + ClassDeclarationSyntax classSyntax = (ClassDeclarationSyntax)context.Node; + IEnumerable attributes = classSyntax.GetHandlerAttributes(); + BaseTypeSyntax? baseType = classSyntax.GetHandlerBaseClass(); + + if (baseType == null && !attributes.Any()) + return null!; + + return new HandlerDeclarationModel(classSyntax, attributes, baseType); + } + + private static void Execute(SourceProductionContext context, ImmutableArray handlers) + { + StringBuilder sourceBuilder = new StringBuilder(); + List usingDirectives = []; + + sourceBuilder + .AppendTabs(0).Append("namespace Telegrator.Analyzers").AppendLine() + .AppendTabs(0).Append("{").AppendLine() + .AppendTabs(1).Append("public static partial class AnalyzerExport").AppendLine() + .AppendTabs(1).Append("{").AppendLine(); + + foreach (HandlerDeclarationModel handler in handlers) + { + context.CancellationToken.ThrowIfCancellationRequested(); + try + { + usingDirectives.UnionAdd(handler.ClassDeclaration.FindCompilationUnitSyntax().Usings.Select(use => use.ToString())); + ParseHandlerDeclaration(context, sourceBuilder, handler, context.CancellationToken); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + sourceBuilder.AppendLine() + .Append("// Failed to parse ").Append(handler.ClassDeclaration.Identifier.ToString()).AppendLine() + .Append(ex).AppendLine(); + } + } + + sourceBuilder.AppendLine("\t}\n}"); + sourceBuilder.Insert(0, string.Join("\n", usingDirectives.OrderBy(use => use)) + "\n\n"); + //context.AddSource("DeveloperHelperAnalyzer.cs", sourceBuilder.ToString()); + } + + private static void ParseHandlerDeclaration(SourceProductionContext context, StringBuilder sourceBuilder, HandlerDeclarationModel handler, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + sourceBuilder.Append("//").Append(handler.ClassDeclaration.Identifier.ToString()).AppendLine(); + //context.ReportDiagnostic(DiagnosticsHelper.Test.Create(handler.ClassDeclaration.Identifier.GetLocation())); + } + } + + internal static class DeveloperHelperAnalyzerExtensions + { + private static readonly string[] AttributeNames = + [ + "AnyUpdateHandlerAttribute", + "CallbackQueryHandlerAttribute", + "CommandHandlerAttribute", + "WelcomeHandlerAttribute", + "MessageHandlerAttribute" + ]; + + private static readonly string[] HandlersNames = + [ + "AnyUpdateHandler", + "CallbackQueryHandler", + "CommandHandler", + "WelcomeHandler", + "MessageHandler" + ]; + + public static IEnumerable GetHandlerAttributes(this ClassDeclarationSyntax classSyntax) + { + IEnumerable attributes = classSyntax.AttributeLists.SelectMany(list => list.Attributes); + return attributes.IntersectBy(AttributeNames, attr => attr.Name.ToString()); + } + + public static BaseTypeSyntax? GetHandlerBaseClass(this ClassDeclarationSyntax classSyntax) + { + return classSyntax.BaseList?.Types.FirstOrDefault(type => HandlersNames.Contains(type.ToString())); + } + } +} diff --git a/Telegrator.Analyzers/DiagnosticsHelper.cs b/Telegrator.Analyzers/DiagnosticsHelper.cs new file mode 100644 index 0000000..2e41103 --- /dev/null +++ b/Telegrator.Analyzers/DiagnosticsHelper.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis; + +namespace Telegrator.Analyzers +{ + public static class DiagnosticsHelper + { + public const string Aspect = "Aspect"; + + public static readonly DiagnosticDescriptor Test = new DiagnosticDescriptor("TR0001", "Test descriptor", string.Empty, Aspect, DiagnosticSeverity.Error, true, "Test diagnostic description."); + + public static Diagnostic Create(this DiagnosticDescriptor descriptor, Location? location, params object[] messageArgs) + => Diagnostic.Create(descriptor, location, messageArgs); + } +} diff --git a/Telegrator.Analyzers/Exceptions.cs b/Telegrator.Analyzers/Exceptions.cs new file mode 100644 index 0000000..10d6301 --- /dev/null +++ b/Telegrator.Analyzers/Exceptions.cs @@ -0,0 +1,12 @@ +namespace Telegrator.Analyzers +{ + /// + /// Exception thrown when a target is not found during code generation. + /// + internal class TargteterNotFoundException() : Exception() { } + + /// + /// Exception thrown when a base class type is not found during code generation. + /// + internal class BaseClassTypeNotFoundException() : Exception() { } +} diff --git a/Telegrator.Analyzers/Models.cs b/Telegrator.Analyzers/Models.cs new file mode 100644 index 0000000..5f1c7e0 --- /dev/null +++ b/Telegrator.Analyzers/Models.cs @@ -0,0 +1,12 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Telegrator.Analyzers +{ + internal class HandlerDeclarationModel(ClassDeclarationSyntax classDeclaration, IEnumerable handlerAttributes, BaseTypeSyntax? baseType) + { + public ClassDeclarationSyntax ClassDeclaration { get; } = classDeclaration; + public IEnumerable HandlerAttributes { get; } = handlerAttributes; + public BaseTypeSyntax? BaseType { get; } = baseType; + public bool HasAttributes => HandlerAttributes.Any(); + } +} diff --git a/Telegrator.Analyzers/Telegrator.Analyzers.csproj b/Telegrator.Analyzers/Telegrator.Analyzers.csproj new file mode 100644 index 0000000..df29263 --- /dev/null +++ b/Telegrator.Analyzers/Telegrator.Analyzers.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + latest + enable + enable + true + Debug;Release;AnalyzersDebug + True + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Telegrator.Analyzers/TypeExtensions.cs b/Telegrator.Analyzers/TypeExtensions.cs new file mode 100644 index 0000000..7d1241a --- /dev/null +++ b/Telegrator.Analyzers/TypeExtensions.cs @@ -0,0 +1,87 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections; +using System.Text; + +namespace Telegrator.Analyzers +{ + internal static class TypeExtensions + { + public static StringBuilder AppendTabs(this StringBuilder builder, int count) + => builder.Append(new string('\t', count)); + + public static string GetBaseTypeSyntaxName(this BaseTypeSyntax baseClassSyntax) + { + if (baseClassSyntax is PrimaryConstructorBaseTypeSyntax parimaryConstructor) + return parimaryConstructor.Type.ToString(); + + if (baseClassSyntax is SimpleBaseTypeSyntax simpleBaseType) + return simpleBaseType.Type.ToString(); + + throw new BaseClassTypeNotFoundException(); + } + + public static bool IsAssignableFrom(this ITypeSymbol? symbol, string className) + { + if (symbol is null) + return false; + + if (symbol.BaseType == null) + return false; + + if (symbol.BaseType.Name == className) + return true; + + return symbol.BaseType.IsAssignableFrom(className); + } + + public static ITypeSymbol? Cast(this ITypeSymbol symbol, string className) + { + if (symbol.BaseType == null) + return null; + + if (symbol.BaseType.Name == className) + return symbol.BaseType; + + return symbol.BaseType.Cast(className); + } + + public static IEnumerable WhereCast(this IEnumerable source) + { + foreach (object value in source) + { + if (value is TResult result) + yield return result; + } + } + + public static CompilationUnitSyntax FindCompilationUnitSyntax(this SyntaxNode syntax) + { + while (syntax is not CompilationUnitSyntax) + syntax = syntax.Parent ?? throw new Exception(); + + return (CompilationUnitSyntax)syntax; + } + + public static IEnumerable IntersectBy(this IEnumerable first, IEnumerable second, Func selector) + { + foreach (TSource item in first) + { + TValue value = selector(item); + if (second.Contains(value)) + yield return item; + } + } + + public static IList UnionAdd(this IList source, IEnumerable toUnion) + { + foreach (TValue toUnionValue in toUnion) + { + if (!source.Contains(toUnionValue, EqualityComparer.Default)) + source.Add(toUnionValue); + } + + return source; + } + } +} diff --git a/Telegrator.Generators/ApiMarkdownGenerator.cs b/Telegrator.Generators/ApiMarkdownGenerator.cs new file mode 100644 index 0000000..93c25e5 --- /dev/null +++ b/Telegrator.Generators/ApiMarkdownGenerator.cs @@ -0,0 +1,267 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Reflection.Metadata.Ecma335; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace Telegrator.Generators +{ + /// + /// Source Generator для автоматической генерации Markdown-документации по публичному API Telegrator. + /// + [Generator] + public class ApiMarkdownGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValueProvider> typeDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (node, _) => node is ClassDeclarationSyntax || node is InterfaceDeclarationSyntax || node is StructDeclarationSyntax || node is EnumDeclarationSyntax, + transform: (ctx, _) => (BaseTypeDeclarationSyntax)ctx.Node + ) + .Where(typeDecl => typeDecl != null) + .Collect(); + + var combined = typeDeclarations.Combine(context.CompilationProvider); + + context.RegisterSourceOutput(combined, (spc, source) => + { + IReadOnlyList typeDecls = source.Left; + Compilation compilation = source.Right; + string markdown = GenerateMarkdown(typeDecls, compilation); + spc.AddSource("TelegramReactive_Api.md", markdown); + }); + } + + private string GenerateMarkdown(IReadOnlyList typeDecls, Compilation compilation) + { + StringBuilder sourceBuilder = new StringBuilder("/*\n"); + + // Writing caution message + sourceBuilder.AppendLine("> [!CAUTION]"); + sourceBuilder.AppendLine("> This page was generated using hand-writen compiler's XML-Summaries output parser"); + sourceBuilder.AppendLine("> If you find any missing syntax, errors in structure, mis-formats or empty sections, please contact owner or open an issue\n"); + + // Collecting only public types + IEnumerable publicTypes = typeDecls + .Select(type => type.TryGetNamedType(compilation)) + .Where(symbol => symbol != null) + .Where(symbol => symbol.DeclaredAccessibility == Accessibility.Public); + + // Grouping by namespace + IOrderedEnumerable> namespaces = publicTypes + .GroupBy(t => t.ContainingNamespace.ToDisplayString()) + .OrderBy(g => g.Key); + + foreach (IGrouping nsGroup in namespaces) + { + string ns = nsGroup.Key == "Telegrator" ? nsGroup.Key : nsGroup.Key.Substring("Telegrator.".Length); + sourceBuilder.AppendFormat("# {0}\n\n", ns); + + foreach (INamedTypeSymbol type in nsGroup.OrderBy(t => t.Name)) + { + // Формируем generic-параметры для заголовка класса + string genericArgs = type.FormatGenericTypes(); + sourceBuilder.AppendFormat("## {0} `{1}{2}`\n\n", type.TypeKind, type.Name, genericArgs); + + string? typeSummary = type.ExtractSummary(); + if (!string.IsNullOrWhiteSpace(typeSummary)) + sourceBuilder.AppendFormat("> {0}\n\n", typeSummary); + + // Writing members + if (type.TypeKind == TypeKind.Enum) + { + WriteEnumValues(sourceBuilder, type); + } + else + { + WriteConstructors(sourceBuilder, type); + WriteProperties(sourceBuilder, type); + WriteMethods(sourceBuilder, type); + } + } + } + + return sourceBuilder.AppendLine().AppendLine("*/").ToString(); + } + + private static void WriteConstructors(StringBuilder sourceBuilder, INamedTypeSymbol type) + { + if (type.TypeKind == TypeKind.Enum) + return; + // Getting ctors + List ctors = type.Constructors + .Where(c => c.DeclaredAccessibility == Accessibility.Public) + .ToList(); + + // Checking for any + if (ctors.Count == 0) + return; + + // Writing + sourceBuilder.AppendLine("**Constructors:**"); + foreach (IMethodSymbol ctor in ctors) + { + // Формируем строку вида ClassName(Param1, Param2) с generic-аргументами + string genericArgs = type.FormatGenericTypes(); + string parameters = string.Join(", ", ctor.Parameters.Select(p => p.Type.GetShortName())); + string signature = string.Format("{0}{1}({2})", type.Name, genericArgs, parameters); + sourceBuilder.Append(" - `").Append(signature).Append("`").AppendLine(); + + // Writing summary + string? propSummary = ctor.ExtractSummary(); + if (!string.IsNullOrWhiteSpace(propSummary)) + sourceBuilder.Append(" > ").Append(propSummary).AppendLine(); + } + + sourceBuilder.AppendLine(); + } + + private static void WriteEnumValues(StringBuilder sourceBuilder, INamedTypeSymbol type) + { + var members = type.GetMembers().OfType().Where(f => f.HasConstantValue && f.DeclaredAccessibility == Accessibility.Public).ToList(); + if (members.Count == 0) + return; + + sourceBuilder.AppendLine("**Values:**"); + foreach (IFieldSymbol field in members) + { + sourceBuilder.Append("- `").Append(field.Name).Append("`"); + string? summary = field.ExtractSummary(); + if (!string.IsNullOrWhiteSpace(summary)) + sourceBuilder.Append(" — ").Append(summary); + sourceBuilder.AppendLine(); + } + sourceBuilder.AppendLine(); + } + + private static void WriteProperties(StringBuilder sourceBuilder, INamedTypeSymbol type) + { + // Getting properties + List props = type + .GetMembers() + .OfType() + .Where(p => p.DeclaredAccessibility == Accessibility.Public) + .ToList(); + + // Checking for any + if (props.Count == 0) + return; + + // Writing + sourceBuilder.AppendLine("**Properties:**"); + foreach (IPropertySymbol prop in props) + { + // Writing member + sourceBuilder.AppendFormat("- `{0}`\n", prop.Name); + + // Writing summary + string? propSummary = prop.ExtractSummary(); + if (!string.IsNullOrWhiteSpace(propSummary)) + sourceBuilder.AppendFormat(" > {0}", propSummary); + + sourceBuilder.AppendLine(); + } + + sourceBuilder.AppendLine(); + } + + private static void WriteMethods(StringBuilder sourceBuilder, INamedTypeSymbol type) + { + // Getting methods + List methods = type + .GetMembers() + .OfType() + .Where(m => m.DeclaredAccessibility == Accessibility.Public) + .Where(m => m.MethodKind == MethodKind.Ordinary) + .ToList(); + + // Checking for any + if (methods.Count == 0) + return; + + // Writing + sourceBuilder.AppendLine("**Methods:**"); + foreach (IMethodSymbol method in methods) + { + // Формируем generic-параметры для метода + string genericArgs = method.FormatGenericTypes(); + string parameters = string.Join(", ", method.Parameters.Select(p => p.Type.GetShortName())); + sourceBuilder.AppendFormat(" - `{0}{1}({2})`\n", method.Name, genericArgs, parameters); + + // Writing summary + string? propSummary = method.ExtractSummary(); + if (!string.IsNullOrWhiteSpace(propSummary)) + sourceBuilder.AppendFormat(" > {0}", propSummary); + + sourceBuilder.AppendLine(); + } + + sourceBuilder.AppendLine(); + } + } + + internal static partial class TypesExtensions + { + public static string? ExtractSummary(this ISymbol symbol) + { + string? xmlDoc = symbol.GetDocumentationCommentXml(); + if (string.IsNullOrWhiteSpace(xmlDoc)) + return null; + + try + { + XDocument doc = XDocument.Parse(xmlDoc); + XElement? summary = doc.Root?.Element("summary"); + + if (summary == null) + return null; + + // Убираем лишние пробелы и переносы строк + return summary.Value.Trim().Replace("\n", " ").Replace(" ", " "); + } + catch + { + // Игнорируем ошибки парсинга XML + return null; + } + } + + public static string FormatGenericTypes(this INamedTypeSymbol methodSymbol) + { + if (methodSymbol.TypeParameters.Length == 0) + return string.Empty; + + string typeParams = string.Join(", ", methodSymbol.TypeParameters.Select(tp => tp.Name)); + return string.Format("<{0}>", typeParams); + } + + public static string FormatGenericTypes(this IMethodSymbol methodSymbol) + { + if (methodSymbol.TypeParameters.Length == 0) + return string.Empty; + + string typeParams = string.Join(", ", methodSymbol.TypeParameters.Select(tp => tp.Name)); + return string.Format("<{0}>", typeParams); + } + + public static string GetShortName(this ITypeSymbol type) + { + if (type is INamedTypeSymbol namedType && namedType.IsGenericType) + { + string genericArgs = string.Join(", ", namedType.TypeArguments.Select(GetShortName)); + return string.Format("{0}<{1}>", namedType.Name, genericArgs); + } + + if (type.TypeKind == TypeKind.TypeParameter) + { + return type.Name; + } + + return type.Name; + } + } +} \ No newline at end of file diff --git a/Telegrator.Generators/Exceptions.cs b/Telegrator.Generators/Exceptions.cs new file mode 100644 index 0000000..896c59b --- /dev/null +++ b/Telegrator.Generators/Exceptions.cs @@ -0,0 +1,12 @@ +namespace Telegrator.Generators +{ + /// + /// Exception thrown when a target is not found during code generation. + /// + internal class TargteterNotFoundException() : Exception() { } + + /// + /// Exception thrown when a base class type is not found during code generation. + /// + internal class BaseClassTypeNotFoundException() : Exception() { } +} diff --git a/Telegrator.Generators/ImplicitHandlerBuilderExtensionsGenerator.cs b/Telegrator.Generators/ImplicitHandlerBuilderExtensionsGenerator.cs new file mode 100644 index 0000000..271c161 --- /dev/null +++ b/Telegrator.Generators/ImplicitHandlerBuilderExtensionsGenerator.cs @@ -0,0 +1,209 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text; + +namespace Telegrator.Generators +{ + [Generator(LanguageNames.CSharp)] + public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { +#if ANALYZERSDEBUG + Debugger.Launch(); +#endif + IncrementalValueProvider> pipeline = context.SyntaxProvider + .CreateSyntaxProvider(SyntaxPredicate, SyntaxTransform) + .Where(declaration => declaration != null) + .Collect(); + + context.RegisterImplementationSourceOutput(pipeline, GenerateSource); + } + + private static bool SyntaxPredicate(SyntaxNode node, CancellationToken _) + { + if (node is not ClassDeclarationSyntax) + return false; + + return true; + } + + private static ClassDeclarationSyntax SyntaxTransform(GeneratorSyntaxContext context, CancellationToken _) + { + ISymbol? symbol = context.SemanticModel.GetDeclaredSymbol(context.Node); + if (symbol is null) + return null!; + + if (symbol is not ITypeSymbol typeSymbol) + return null!; + + if (!typeSymbol.IsAssignableFrom("UpdateFilterAttribute")) + return null!; + + return (ClassDeclarationSyntax)context.Node; + } + + private static void GenerateSource(SourceProductionContext context, ImmutableArray declarations) + { + StringBuilder source = new StringBuilder(); + Dictionary targeters = []; + List usingDirectives = + [ + "using Telegrator.Handlers.Building;", + "using Telegrator.Handlers.Building.Components;" + ]; + + StringBuilder sourceBuilder = new StringBuilder() + .AppendLine("namespace Telegrator") + .AppendLine("{") + .Append("\t//").Append(string.Join(", ", declarations.Select(decl => decl.Identifier.ToString()))).AppendLine() + .AppendLine("\tpublic static partial class HandlerBuilderExtensions") + .AppendLine("\t{"); + + List lateTargeterClasses = []; + foreach (ClassDeclarationSyntax classDeclaration in declarations) + { + try + { + usingDirectives.UnionAdd(classDeclaration.FindCompilationUnitSyntax().Usings.Select(use => use.ToString())); + ParseClassDeclaration(sourceBuilder, classDeclaration, targeters); + } + catch (TargteterNotFoundException) + { + lateTargeterClasses.Add(classDeclaration); + } + catch (Exception exc) + { + string errorFormat = string.Format("\t\t// failed to generate for {0} : {1}", classDeclaration.Identifier.ToString(), exc.GetType().Name); + sourceBuilder.AppendLine(errorFormat); + } + } + + foreach (ClassDeclarationSyntax classDeclaration in lateTargeterClasses) + { + try + { + usingDirectives.UnionAdd(classDeclaration.FindCompilationUnitSyntax().Usings.Select(use => use.ToString())); + ParseClassDeclaration(sourceBuilder, classDeclaration, targeters); + } + catch (Exception exc) + { + string errorFormat = string.Format("\t\t// failed to generate for {0} : {1}", classDeclaration.Identifier.ToString(), exc.GetType().Name); + sourceBuilder.AppendLine(errorFormat); + } + } + + sourceBuilder.AppendLine("\t}\n}"); + sourceBuilder.Insert(0, string.Join("\n", usingDirectives.Select(use => use.ToString()).OrderBy(use => use)) + "\n\n"); + context.AddSource("GeneratedHandlerBuilderExtensions.cs", sourceBuilder.ToString()); + } + + private static void ParseClassDeclaration(StringBuilder sourceBuilder, ClassDeclarationSyntax classDeclaration, Dictionary targeters) + { + IEnumerable methods = classDeclaration.Members.OfType(); + MethodDeclarationSyntax? targeterMethod = methods.FirstOrDefault(method => method.Identifier.ToString() == "GetFilterringTarget"); + + string className = classDeclaration.Identifier.ToString(); + string filterName = className.Replace("Attribute", string.Empty); + string classTargetterMethodName = filterName + "_GetFilterringTarget"; + + if (targeterMethod != null) + { + targeters.Add(className, classTargetterMethodName); + RenderTargeterMethod(sourceBuilder, classTargetterMethodName, targeterMethod); + sourceBuilder.AppendLine(); + } + else + { + if (classDeclaration.BaseList == null) + throw new Exception(); + + string baseClassName = classDeclaration.BaseList.Types + .ElementAt(0).GetBaseTypeSyntaxName(); + + if (!targeters.ContainsKey(baseClassName)) + throw new TargteterNotFoundException(); + + classTargetterMethodName = targeters[baseClassName]; + } + + if (classDeclaration.Modifiers.Any(keyword => keyword.ValueText == "abstract")) + return; + + if (classDeclaration.ParameterList != null) + { + if (classDeclaration.BaseList != null) + { + PrimaryConstructorBaseTypeSyntax primaryConstructor = (PrimaryConstructorBaseTypeSyntax)classDeclaration.BaseList.Types.ElementAt(0); + RenderExtensionMethod(sourceBuilder, filterName, classTargetterMethodName, classDeclaration.ParameterList.Parameters, primaryConstructor.ArgumentList.Arguments); + } + else + { + RenderExtensionMethod(sourceBuilder, filterName, classTargetterMethodName, classDeclaration.ParameterList.Parameters, []); + } + + sourceBuilder.AppendLine(); + } + + foreach (ConstructorDeclarationSyntax constructor in classDeclaration.Members.OfType()) + { + if (constructor.Initializer == null) + continue; + + RenderExtensionMethod(sourceBuilder, filterName, classTargetterMethodName, constructor.ParameterList.Parameters, constructor.Initializer.ArgumentList.Arguments); + sourceBuilder.AppendLine(); + } + } + + private static void RenderExtensionMethod(StringBuilder sourceBuilder, string filterName, string classTargetterMethodName, SeparatedSyntaxList parameters, SeparatedSyntaxList arguments) + { + if (filterName == "ChatType") + filterName = "InChatType"; // Because it conflicting + + sourceBuilder + .Append("\t\t/// ").AppendLine() + .Append("\t\t/// Adds ").Append(filterName).Append(" filter to implicit handler").AppendLine() + .Append("\t\t/// ").AppendLine(); + + sourceBuilder.Append("\t\tpublic static TBuilder ").Append(filterName).Append("(this TBuilder builder"); + + if (parameters.Any()) + sourceBuilder.Append(", ").Append(parameters.ToFullString()); + + sourceBuilder + .Append(") where TBuilder : IHandlerBuilder").AppendLine() + .Append("\t\t{").AppendLine() + .Append("\t\t\tbuilder.AddTargetedFilter"); + + if (arguments.Count > 1) + sourceBuilder.Append("s"); + + sourceBuilder.Append("(").Append(classTargetterMethodName); + + if (arguments.Any()) + sourceBuilder.Append(", ").Append(arguments.ToFullString()); + + sourceBuilder + .Append(");").AppendLine() + .Append("\t\t\treturn builder;").AppendLine() + .Append("\t\t}").AppendLine(); + } + + private static void RenderTargeterMethod(StringBuilder sourceBuilder, string classTargetterMethodName, MethodDeclarationSyntax targeterMethod) + { + sourceBuilder.Append("\t\tprivate static ").Append(targeterMethod.ReturnType.ToString()).Append(" ").Append(classTargetterMethodName).Append(targeterMethod.ParameterList.ToFullString()); + + if (targeterMethod.ExpressionBody != null) + { + sourceBuilder.Append(targeterMethod.ExpressionBody.ToFullString()).Append(";").AppendLine(); + } + else if (targeterMethod.Body != null) + { + sourceBuilder.Append(targeterMethod.Body.ToFullString()); + } + } + } +} diff --git a/Telegrator.Generators/Telegrator.Generators.csproj b/Telegrator.Generators/Telegrator.Generators.csproj new file mode 100644 index 0000000..440b258 --- /dev/null +++ b/Telegrator.Generators/Telegrator.Generators.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + latest + enable + enable + true + Debug;Release;AnalyzersDebug + True + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Telegrator.Generators/TypeExtensions.cs b/Telegrator.Generators/TypeExtensions.cs new file mode 100644 index 0000000..a6aa23d --- /dev/null +++ b/Telegrator.Generators/TypeExtensions.cs @@ -0,0 +1,67 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Telegrator.Generators +{ + internal static partial class TypeExtensions + { + public static INamedTypeSymbol TryGetNamedType(this BaseTypeDeclarationSyntax syntax, Compilation compilation) + { + SemanticModel semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree); + return semanticModel.GetDeclaredSymbol(syntax)!; + } + + public static string GetBaseTypeSyntaxName(this BaseTypeSyntax baseClassSyntax) + { + if (baseClassSyntax is PrimaryConstructorBaseTypeSyntax parimaryConstructor) + return parimaryConstructor.Type.ToString(); + + if (baseClassSyntax is SimpleBaseTypeSyntax simpleBaseType) + return simpleBaseType.Type.ToString(); + + throw new BaseClassTypeNotFoundException(); + } + + public static bool IsAssignableFrom(this ITypeSymbol symbol, string className) + { + if (symbol.BaseType == null) + return false; + + if (symbol.BaseType.Name == className) + return true; + + return symbol.BaseType.IsAssignableFrom(className); + } + + public static ITypeSymbol? Cast(this ITypeSymbol symbol, string className) + { + if (symbol.BaseType == null) + return null; + + if (symbol.BaseType.Name == className) + return symbol.BaseType; + + return symbol.BaseType.Cast(className); + } + + public static CompilationUnitSyntax FindCompilationUnitSyntax(this SyntaxNode syntax) + { + while (syntax is not CompilationUnitSyntax) + syntax = syntax.Parent ?? throw new Exception(); + + return (CompilationUnitSyntax)syntax; + } + + public static IList UnionAdd(this IList source, IEnumerable toUnion) + { + foreach (TValue toUnionValue in toUnion) + { + if (!source.Contains(toUnionValue, EqualityComparer.Default)) + source.Add(toUnionValue); + } + + return source; + } + } +} diff --git a/Telegrator.Hosting.Web/Components/ITelegramBotWebHost.cs b/Telegrator.Hosting.Web/Components/ITelegramBotWebHost.cs new file mode 100644 index 0000000..5ab2dc0 --- /dev/null +++ b/Telegrator.Hosting.Web/Components/ITelegramBotWebHost.cs @@ -0,0 +1,9 @@ +using Telegrator.Hosting.Components; + +namespace Telegrator.Hosting.Web.Components +{ + public interface ITelegramBotWebHost : ITelegramBotHost//, IEndpointRouteBuilder + { + + } +} diff --git a/Telegrator.Hosting.Web/Polling/HostedUpdateWebhooker.cs b/Telegrator.Hosting.Web/Polling/HostedUpdateWebhooker.cs new file mode 100644 index 0000000..36cd5e7 --- /dev/null +++ b/Telegrator.Hosting.Web/Polling/HostedUpdateWebhooker.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Telegram.Bot; +using Telegrator.Hosting.Web.Components; +using Telegrator.MadiatorCore; + +namespace Telegrator.Hosting.Web.Polling +{ + public class HostedUpdateWebhooker : IHostedService + { + private readonly ITelegramBotWebHost _botHost; + private readonly ITelegramBotClient _botClient; + private readonly IUpdateRouter _updateRouter; + private readonly TelegramBotWebOptions _options; + + public HostedUpdateWebhooker(ITelegramBotWebHost botHost, ITelegramBotClient botClient, IUpdateRouter updateRouter, IOptions options) + { + if (string.IsNullOrEmpty(options.Value.WebhookUri)) + throw new ArgumentNullException(nameof(options), "Option \"WebhookUrl\" must be set to subscribe for update recieving"); + + if (string.IsNullOrEmpty(options.Value.WebhookPattern)) + throw new ArgumentNullException(nameof(options), "Option \"WebhookPattern\" must be set to subscribe for update recieving"); + + _botHost = botHost; + _botClient = botClient; + _updateRouter = updateRouter; + _options = options.Value; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _botClient.SetWebhook( + url: _options.WebhookUri, + maxConnections: _options.MaxConnections, + allowedUpdates: _botHost.UpdateRouter.HandlersProvider.AllowedTypes, + dropPendingUpdates: _options.DropPendingUpdates, + cancellationToken: cancellationToken); + + //botHost.MapGet(_options.WebhookPattern, async (Update update) => await _updateRouter.HandleUpdateAsync(_botClient, update, cancellationToken)); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _botClient.DeleteWebhook(_options.DropPendingUpdates, cancellationToken); + return Task.CompletedTask; + } + } +} diff --git a/Telegrator.Hosting.Web/TelegramBotWebHost.cs b/Telegrator.Hosting.Web/TelegramBotWebHost.cs new file mode 100644 index 0000000..8e4dc29 --- /dev/null +++ b/Telegrator.Hosting.Web/TelegramBotWebHost.cs @@ -0,0 +1,10 @@ +using Telegrator.Hosting.Components; +using Microsoft.AspNetCore.Routing; + +namespace Telegrator.Hosting.Web +{ + public class TelegramBotWebHost //: ITelegramBotWebHost + { + + } +} diff --git a/Telegrator.Hosting.Web/TelegramBotWebHostBuilder.cs b/Telegrator.Hosting.Web/TelegramBotWebHostBuilder.cs new file mode 100644 index 0000000..96b1202 --- /dev/null +++ b/Telegrator.Hosting.Web/TelegramBotWebHostBuilder.cs @@ -0,0 +1,10 @@ +using Telegrator.Hosting.Components; +using Telegrator.Hosting.Web.Components; + +namespace Telegrator.Hosting.Web +{ + public class TelegramBotWebHostBuilder //: ITelegramBotHostBuilder + { + + } +} diff --git a/Telegrator.Hosting.Web/TelegramBotWebOptions.cs b/Telegrator.Hosting.Web/TelegramBotWebOptions.cs new file mode 100644 index 0000000..6e2eceb --- /dev/null +++ b/Telegrator.Hosting.Web/TelegramBotWebOptions.cs @@ -0,0 +1,18 @@ +using Telegrator.Configuration; + +namespace Telegrator.Hosting.Web +{ + public class TelegramBotWebOptions : TelegramBotOptions + { + /// + /// Gets or sets uri for webhook update receiving + /// + public required string WebhookUri { get; set; } + + public required string WebhookPattern { get; set; } + + public int MaxConnections { get; set; } + + public bool DropPendingUpdates { get; set; } + } +} diff --git a/Telegrator.Hosting.Web/Telegrator.Hosting.Web.csproj b/Telegrator.Hosting.Web/Telegrator.Hosting.Web.csproj new file mode 100644 index 0000000..a9bf70f --- /dev/null +++ b/Telegrator.Hosting.Web/Telegrator.Hosting.Web.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/Telegrator.Hosting.Web/TypesExtensions.cs b/Telegrator.Hosting.Web/TypesExtensions.cs new file mode 100644 index 0000000..7cb7f67 --- /dev/null +++ b/Telegrator.Hosting.Web/TypesExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Telegram.Bot; +using Telegrator.Hosting.Web.Polling; + +namespace Telegrator.Hosting.Web +{ + public static class ServicesCollectionExtensions + { + public static IServiceCollection AddTelegramWebhook(this IServiceCollection services) + { + services.AddHttpClient("tgwebhook").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory); + services.AddHostedService(); + return services; + } + + private static ITelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider) + => new TelegramBotClient(provider.GetRequiredService>().Value, httpClient); + } +} diff --git a/Telegrator.Hosting/Components/IPreBuildingRoutine.cs b/Telegrator.Hosting/Components/IPreBuildingRoutine.cs new file mode 100644 index 0000000..8e1fd14 --- /dev/null +++ b/Telegrator.Hosting/Components/IPreBuildingRoutine.cs @@ -0,0 +1,15 @@ +namespace Telegrator.Hosting.Components +{ + /// + /// Interface for pre-building routines that can be executed during host construction. + /// Allows for custom initialization and configuration steps before the bot host is built. + /// + public interface IPreBuildingRoutine + { + /// + /// Executes the pre-building routine on the specified host builder. + /// + /// The host builder to configure. + public static abstract void PreBuildingRoutine(TelegramBotHostBuilder hostBuilder); + } +} diff --git a/Telegrator.Hosting/Components/ITelegramBotHost.cs b/Telegrator.Hosting/Components/ITelegramBotHost.cs new file mode 100644 index 0000000..956dc58 --- /dev/null +++ b/Telegrator.Hosting/Components/ITelegramBotHost.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Hosting; +using Telegrator; + +namespace Telegrator.Hosting.Components +{ + /// + /// Interface for Telegram bot hosts. + /// Combines host application capabilities with reactive Telegram bot functionality. + /// + public interface ITelegramBotHost : IHost, IReactiveTelegramBot + { + + } +} diff --git a/Telegrator.Hosting/Components/ITelegramBotHostBuilder.cs b/Telegrator.Hosting/Components/ITelegramBotHostBuilder.cs new file mode 100644 index 0000000..69b40bd --- /dev/null +++ b/Telegrator.Hosting/Components/ITelegramBotHostBuilder.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Telegrator.MadiatorCore; + +namespace Telegrator.Hosting.Components +{ + /// + /// Interface for building Telegram bot hosts with dependency injection support. + /// Combines host application building capabilities with handler collection functionality. + /// + public interface ITelegramBotHostBuilder : ICollectingProvider + { + /// + /// Gets the set of key/value configuration properties. + /// + IConfigurationManager Configuration { get; } + + /// + /// Gets a collection of logging providers for the application to compose. This is useful for adding new logging providers. + /// + ILoggingBuilder Logging { get; } + + /// + /// Gets a collection of services for the application to compose. This is useful for adding user provided or framework provided services. + /// + IServiceCollection Services { get; } + } +} diff --git a/Telegrator.Hosting/Configuration/ConfigureOptionsProxy.cs b/Telegrator.Hosting/Configuration/ConfigureOptionsProxy.cs new file mode 100644 index 0000000..51022b4 --- /dev/null +++ b/Telegrator.Hosting/Configuration/ConfigureOptionsProxy.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Telegrator.Hosting.Configuration +{ + /// + /// Abstract base class for configuring options from configuration sources. + /// Provides a proxy pattern for binding configuration to strongly-typed options classes. + /// + /// The type of options to configure. + public abstract class ConfigureOptionsProxy where TOptions : class + { + /// + /// Configures the options using the default configuration section. + /// + /// The service collection to configure. + /// The configuration source. + public void Configure(IServiceCollection services, IConfiguration configuration) + => Configure(services, Options.DefaultName, configuration, null); + + /// + /// Configures the options using a named configuration section. + /// + /// The service collection to configure. + /// The name of the configuration section. + /// The configuration source. + public void Configure(IServiceCollection services, string? name, IConfiguration configuration) + => Configure(services, name, configuration, null); + + /// + /// Configures the options using a named configuration section with custom binder options. + /// + /// The service collection to configure. + /// The name of the configuration section. + /// The configuration source. + /// Optional action to configure the binder options. + public void Configure(IServiceCollection services, string? name, IConfiguration configuration, Action? configureBinder) + { + var namedConfigure = new NamedConfigureFromConfigurationOptions>(name, configuration, configureBinder); + namedConfigure.Configure(name, this); + + services.AddOptions(); + services.AddSingleton(Options.Create(Realize())); + } + + /// + /// Creates the actual options instance from the configuration. + /// + /// The configured options instance. + protected abstract TOptions Realize(); + } +} diff --git a/Telegrator.Hosting/Configuration/TelegramBotClientOptionsProxy.cs b/Telegrator.Hosting/Configuration/TelegramBotClientOptionsProxy.cs new file mode 100644 index 0000000..1f43344 --- /dev/null +++ b/Telegrator.Hosting/Configuration/TelegramBotClientOptionsProxy.cs @@ -0,0 +1,46 @@ +using Telegram.Bot; + +namespace Telegrator.Hosting.Configuration +{ + /// + /// Internal proxy class for configuring Telegram bot client options from configuration. + /// Extends ConfigureOptionsProxy to provide specific configuration for Telegram bot client options. + /// + internal class TelegramBotClientOptionsProxy : ConfigureOptionsProxy + { + /// + /// Gets or sets the bot token. + /// + public string Token { get; set; } = string.Empty; + + /// + /// Gets or sets the base URL for the bot API. + /// + public string? BaseUrl { get; set; } = null; + + /// + /// Gets or sets whether to use the test environment. + /// + public bool UseTestEnvironment { get; set; } = false; + + /// + /// Gets or sets the retry threshold in seconds. + /// + public int RetryThreshold { get; set; } = 60; + + /// + /// Gets or sets the number of retry attempts. + /// + public int RetryCount { get; set; } = 3; + + /// + /// Creates a TelegramBotClientOptions instance from the proxy configuration. + /// + /// The configured TelegramBotClientOptions instance. + protected override TelegramBotClientOptions Realize() => new TelegramBotClientOptions(Token, BaseUrl, UseTestEnvironment) + { + RetryCount = RetryCount, + RetryThreshold = RetryThreshold + }; + } +} diff --git a/Telegrator.Hosting/GlobalSuppressions.cs b/Telegrator.Hosting/GlobalSuppressions.cs new file mode 100644 index 0000000..7f9eb35 --- /dev/null +++ b/Telegrator.Hosting/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0290")] diff --git a/Telegrator.Hosting/Polling/HostUpdateHandlersPool.cs b/Telegrator.Hosting/Polling/HostUpdateHandlersPool.cs new file mode 100644 index 0000000..5593518 --- /dev/null +++ b/Telegrator.Hosting/Polling/HostUpdateHandlersPool.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Telegrator.Configuration; +using Telegrator.MadiatorCore.Descriptors; +using Telegrator.Polling; + +namespace Telegrator.Hosting.Polling +{ + public class HostUpdateHandlersPool(IOptions options, ILogger logger) + : UpdateHandlersPool(options.Value, options.Value.GlobalCancellationToken) + { + private readonly ILogger _logger = logger; + + protected override async Task ExecuteHandlerWrapper(DescribedHandlerInfo enqueuedHandler) + { + _logger.LogInformation("Handler \"{0}\" has entered execution pool", enqueuedHandler.DisplayString); + await base.ExecuteHandlerWrapper(enqueuedHandler); + } + } +} diff --git a/Telegrator.Hosting/Polling/HostUpdateRouter.cs b/Telegrator.Hosting/Polling/HostUpdateRouter.cs new file mode 100644 index 0000000..94b9f6c --- /dev/null +++ b/Telegrator.Hosting/Polling/HostUpdateRouter.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegrator; +using Telegrator.Configuration; +using Telegrator.MadiatorCore; +using Telegrator.Polling; + +namespace Telegrator.Hosting.Polling +{ + public class HostUpdateRouter : UpdateRouter + { + protected readonly ILogger Logger; + + public HostUpdateRouter(IHandlersProvider handlersProvider, IAwaitingProvider awaitingProvider, IOptions options, IUpdateHandlersPool handlersPool, ILogger logger) + : base(handlersProvider, awaitingProvider, options.Value, handlersPool) + { + Logger = logger; + ExceptionHandler = new HostExceptionHandler(logger); + } + + public override Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) + { + Logger.LogInformation("Received update of type \"{type}\"", update.Type); + return base.HandleUpdateAsync(botClient, update, cancellationToken); + } + + private class HostExceptionHandler(ILogger logger) : IRouterExceptionHandler + { + public void HandleException(ITelegramBotClient botClient, Exception exception, HandleErrorSource source, CancellationToken cancellationToken) + { + if (exception is HandlerFaultedException handlerFaultedException) + { + logger.LogError("\"{handler}\" handler's execution was faulted :\n{exception}", + handlerFaultedException.HandlerInfo.DisplayString, + handlerFaultedException.InnerException?.ToString() ?? "No inner exception"); + return; + } + + logger.LogError("Exception was thrown during update routing faulted :\n{exception}", exception.ToString()); + } + } + } +} diff --git a/Telegrator.Hosting/Polling/HostedUpdateReceiver.cs b/Telegrator.Hosting/Polling/HostedUpdateReceiver.cs new file mode 100644 index 0000000..4ac2592 --- /dev/null +++ b/Telegrator.Hosting/Polling/HostedUpdateReceiver.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegrator.Hosting.Components; +using Telegrator.MadiatorCore; +using Telegrator.Polling; + +namespace Telegrator.Hosting.Polling +{ + public class HostedUpdateReceiver(ITelegramBotHost botHost, ITelegramBotClient botClient, IUpdateRouter updateRouter, IOptions options, ILogger logger) : BackgroundService + { + private readonly ReceiverOptions ReceiverOptions = options.Value; + private readonly IUpdateRouter UpdateRouter = updateRouter; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("Starting receiving updates via long-polling"); + ReceiverOptions.AllowedUpdates = botHost.UpdateRouter.HandlersProvider.AllowedTypes.ToArray(); + ReactiveUpdateReceiver updateReceiver = new ReactiveUpdateReceiver(botClient, ReceiverOptions); + await updateReceiver.ReceiveAsync(UpdateRouter, stoppingToken).ConfigureAwait(false); + } + } +} diff --git a/Telegrator.Hosting/Providers/HostAwaitingProvider.cs b/Telegrator.Hosting/Providers/HostAwaitingProvider.cs new file mode 100644 index 0000000..56b9667 --- /dev/null +++ b/Telegrator.Hosting/Providers/HostAwaitingProvider.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegrator.Configuration; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; +using Telegrator.Providers; + +namespace Telegrator.Hosting.Providers +{ + public class HostAwaitingProvider(IOptions options, ITelegramBotInfo botInfo, ILogger logger) : AwaitingProvider(options.Value, botInfo) + { + public override IEnumerable GetHandlers(IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default) + { + IEnumerable handlers = base.GetHandlers(updateRouter, client, update, cancellationToken).ToArray(); + logger.LogInformation("Described awaiting handlers : {handlers}", string.Join(", ", handlers.Select(hndlr => hndlr.HandlerInstance.GetType().Name))); + return handlers; + } + } +} diff --git a/Telegrator.Hosting/Providers/HostHandlersCollection.cs b/Telegrator.Hosting/Providers/HostHandlersCollection.cs new file mode 100644 index 0000000..5175120 --- /dev/null +++ b/Telegrator.Hosting/Providers/HostHandlersCollection.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; +using Telegrator.Configuration; +using Telegrator.Hosting.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; +using Telegrator.Providers; + +namespace Telegrator.Hosting.Providers +{ + public class HostHandlersCollection(IServiceCollection hostServiceColletion, IHandlersCollectingOptions options) : HandlersCollection(options) + { + private readonly IServiceCollection Services = hostServiceColletion; + public readonly List> PreBuilderRoutines = []; + protected override bool MustHaveParameterlessCtor => false; + + public override IHandlersCollection AddHandler(Type handlerType) + { + // + if (handlerType.GetInterface(nameof(IPreBuildingRoutine)) != null) + { + MethodInfo? methodInfo = handlerType.GetMethod(nameof(IPreBuildingRoutine.PreBuildingRoutine), BindingFlags.Static | BindingFlags.Public); + if (methodInfo != null) + { + Action routineDelegate = methodInfo.CreateDelegate>(null); + PreBuilderRoutines.Add(routineDelegate); + } + } + + return base.AddHandler(handlerType); + } + + public override IHandlersCollection AddDescriptor(HandlerDescriptor descriptor) + { + switch (descriptor.Type) + { + case DescriptorType.General: + { + if (descriptor.InstanceFactory != null) + Services.AddScoped(descriptor.HandlerType, _ => descriptor.InstanceFactory.Invoke()); + else + Services.AddScoped(descriptor.HandlerType); + + break; + } + + case DescriptorType.Keyed: + { + if (descriptor.InstanceFactory != null) + Services.AddKeyedScoped(descriptor.HandlerType, descriptor.ServiceKey, (_, _) => descriptor.InstanceFactory.Invoke()); + else + Services.AddKeyedScoped(descriptor.HandlerType, descriptor.ServiceKey); + + break; + } + + case DescriptorType.Singleton: + { + Services.AddSingleton(descriptor.HandlerType, descriptor.SingletonInstance ?? (descriptor.InstanceFactory != null + ? descriptor.InstanceFactory.Invoke() + : throw new Exception())); + + break; + } + + case DescriptorType.Implicit: + { + Services.AddKeyedSingleton(descriptor.HandlerType, descriptor.ServiceKey, descriptor.SingletonInstance ?? (descriptor.InstanceFactory != null + ? descriptor.InstanceFactory.Invoke() + : throw new Exception())); + + break; + } + } + + return base.AddDescriptor(descriptor); + } + } +} diff --git a/Telegrator.Hosting/Providers/HostHandlersProvider.cs b/Telegrator.Hosting/Providers/HostHandlersProvider.cs new file mode 100644 index 0000000..60e73b3 --- /dev/null +++ b/Telegrator.Hosting/Providers/HostHandlersProvider.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegrator.Configuration; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; +using Telegrator.Providers; + +namespace Telegrator.Hosting.Providers +{ + public class HostHandlersProvider : HandlersProvider + { + private readonly IServiceProvider Services; + private readonly ILogger Logger; + + public HostHandlersProvider(IHandlersCollection handlers, IOptions options, ITelegramBotInfo botInfo, IServiceProvider serviceProvider, ILogger logger) + : base(handlers, options.Value, botInfo) + { + Services = serviceProvider; + Logger = logger; + } + + public override IEnumerable GetHandlers(IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default) + { + IEnumerable handlers = base.GetHandlers(updateRouter, client, update, cancellationToken).ToArray(); + Logger.LogInformation("Described handlers : {handlers}", string.Join(", ", handlers.Select(hndlr => hndlr.DisplayString ?? hndlr.HandlerInstance.GetType().Name))); + return handlers; + } + + public override UpdateHandlerBase GetHandlerInstance(HandlerDescriptor descriptor, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + IServiceScope scope = Services.CreateScope(); + object handlerInstance = descriptor.ServiceKey == null + ? scope.ServiceProvider.GetRequiredService(descriptor.HandlerType) + : scope.ServiceProvider.GetRequiredKeyedService(descriptor.HandlerType, descriptor.ServiceKey); + + if (handlerInstance is not UpdateHandlerBase updateHandler) + throw new InvalidOperationException(); + + updateHandler.LifetimeToken.OnLifetimeEnded += _ => scope.Dispose(); + return updateHandler; + } + } +} diff --git a/Telegrator.Hosting/TelegramBotHost.cs b/Telegrator.Hosting/TelegramBotHost.cs new file mode 100644 index 0000000..ea6164d --- /dev/null +++ b/Telegrator.Hosting/TelegramBotHost.cs @@ -0,0 +1,118 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Text; +using Telegram.Bot.Types.Enums; +using Telegrator.Hosting.Components; +using Telegrator.Hosting.Providers; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Hosting +{ + public class TelegramBotHost : ITelegramBotHost + { + private readonly IHost _innerHost; + private readonly IUpdateRouter _updateRouter; + private readonly ILogger _logger; + + private bool _disposed; + + /// + public IServiceProvider Services => _innerHost.Services; + + /// + public IUpdateRouter UpdateRouter => _updateRouter; + + /// + /// This application's logger + /// + public ILogger Logger => _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider. + internal TelegramBotHost(HostApplicationBuilder hostApplicationBuilder, HostHandlersCollection handlers) + { + RegisterHostServices(hostApplicationBuilder, handlers); + _innerHost = hostApplicationBuilder.Build(); + + _updateRouter = Services.GetRequiredService(); + _logger = Services.GetRequiredService>(); + + LogHandlers(handlers); + } + + public static TelegramBotHostBuilder CreateBuilder() + { + TelegramBotHostBuilder builder = new TelegramBotHostBuilder(null); + builder.Services.AddTelegramBotHostDefaults(); + builder.Services.AddTelegramReceiver(); + return builder; + } + + public static TelegramBotHostBuilder CreateBuilder(TelegramBotHostBuilderSettings? settings) + { + TelegramBotHostBuilder builder = new TelegramBotHostBuilder(settings); + builder.Services.AddTelegramBotHostDefaults(); + builder.Services.AddTelegramReceiver(); + return builder; + } + + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await _innerHost.StartAsync(cancellationToken); + } + + /// + public async Task StopAsync(CancellationToken cancellationToken = default) + { + await _innerHost.StopAsync(cancellationToken); + } + + /// + /// Disposes the host. + /// + public void Dispose() + { + if (_disposed) + return; + + GC.SuppressFinalize(this); + _disposed = true; + } + + private void LogHandlers(HostHandlersCollection handlers) + { + StringBuilder logBuilder = new StringBuilder("Registered handlers : "); + if (!handlers.Keys.Any()) + throw new Exception(); + + foreach (UpdateType updateType in handlers.Keys) + { + HandlerDescriptorList descriptors = handlers[updateType]; + logBuilder.AppendLine("\n\tUpdateType." + updateType + " :"); + + foreach (HandlerDescriptor descriptor in descriptors.Reverse()) + { + string indexerString = descriptor.Indexer.ToString(); + logBuilder.AppendLine("* " + indexerString + " " + (descriptor.DisplayString ?? descriptor.HandlerType.Name)); + } + } + + Logger.LogInformation(logBuilder.ToString()); + } + + private void RegisterHostServices(HostApplicationBuilder hostApplicationBuilder, HostHandlersCollection handlers) + { + //hostApplicationBuilder.Services.RemoveAll(); + //hostApplicationBuilder.Services.AddSingleton(this); + + hostApplicationBuilder.Services.AddSingleton(this); + hostApplicationBuilder.Services.AddSingleton(this); + hostApplicationBuilder.Services.AddSingleton(handlers); + } + } +} diff --git a/Telegrator.Hosting/TelegramBotHostBuilder.cs b/Telegrator.Hosting/TelegramBotHostBuilder.cs new file mode 100644 index 0000000..9d160bd --- /dev/null +++ b/Telegrator.Hosting/TelegramBotHostBuilder.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegrator.Hosting.Configuration; +using Telegrator.Configuration; +using Telegrator.Hosting; +using Telegrator.Hosting.Components; +using Telegrator.Hosting.Providers; +using Telegrator.MadiatorCore; + +#pragma warning disable IDE0001 +namespace Telegrator.Hosting +{ + public class TelegramBotHostBuilder : ITelegramBotHostBuilder + { + private readonly HostApplicationBuilder _innerBuilder; + private readonly TelegramBotHostBuilderSettings _settings; + private readonly HostHandlersCollection _handlers; + + /// + public IHandlersCollection Handlers => _handlers; + + /// + public IServiceCollection Services => _innerBuilder.Services; + + /// + public IConfigurationManager Configuration => _innerBuilder.Configuration; + + /// + public ILoggingBuilder Logging => _innerBuilder.Logging; + + /// + public IHostEnvironment Environment => _innerBuilder.Environment; + + /// + /// Initializes a new instance of the class. + /// + internal TelegramBotHostBuilder(TelegramBotHostBuilderSettings? settings = null) + { + _settings = settings ?? new TelegramBotHostBuilderSettings(); + _innerBuilder = new HostApplicationBuilder(settings?.ToApplicationBuilderSettings()); + _handlers = new HostHandlersCollection(Services, _settings); + + Services.Configure(Configuration.GetSection(nameof(TelegramBotOptions))); + Services.Configure(Configuration.GetSection(nameof(ReceiverOptions))); + Services.Configure(Configuration.GetSection(nameof(TelegramBotClientOptions)), new TelegramBotClientOptionsProxy()); + } + + /// + /// Builds the host. + /// + /// + public TelegramBotHost Build() + { + foreach (var preBuildRoutine in _handlers.PreBuilderRoutines) + { + try + { + preBuildRoutine.Invoke(this); + } + catch (NotImplementedException) + { + _ = 0xBAD + 0xC0DE; + } + } + + return new TelegramBotHost(_innerBuilder, _handlers); + } + } +} diff --git a/Telegrator.Hosting/TelegramBotHostBuilderSettings.cs b/Telegrator.Hosting/TelegramBotHostBuilderSettings.cs new file mode 100644 index 0000000..74b3d0f --- /dev/null +++ b/Telegrator.Hosting/TelegramBotHostBuilderSettings.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Telegrator.Configuration; + +namespace Telegrator.Hosting +{ + /// + /// + /// + public class TelegramBotHostBuilderSettings() : IHandlersCollectingOptions + { + /// + public bool DisableDefaults { get; set; } + + /// + public string[]? Args { get; set; } + + /// + public ConfigurationManager? Configuration { get; set; } + + /// + public string? EnvironmentName { get; set; } + + /// + public string? ApplicationName { get; set; } + + /// + public string? ContentRootPath { get; set; } + + /// + public bool DescendDescriptorIndex { get; set; } = true; + + /// + public bool ExceptIntersectingCommandAliases { get; set; } = true; + + internal HostApplicationBuilderSettings ToApplicationBuilderSettings() => new HostApplicationBuilderSettings() + { + DisableDefaults = DisableDefaults, + Args = Args, + Configuration = Configuration, + EnvironmentName = EnvironmentName, + ApplicationName = ApplicationName, + ContentRootPath = ContentRootPath + }; + } +} diff --git a/Telegrator.Hosting/Telegrator.Hosting.csproj b/Telegrator.Hosting/Telegrator.Hosting.csproj new file mode 100644 index 0000000..0c509e6 --- /dev/null +++ b/Telegrator.Hosting/Telegrator.Hosting.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + Debug;Release + + + + + + + + + + + + diff --git a/Telegrator.Hosting/TypesExtensions.cs b/Telegrator.Hosting/TypesExtensions.cs new file mode 100644 index 0000000..7db6fc6 --- /dev/null +++ b/Telegrator.Hosting/TypesExtensions.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegrator; +using Telegrator.Configuration; +using Telegrator.Hosting.Components; +using Telegrator.Hosting.Configuration; +using Telegrator.Hosting.Polling; +using Telegrator.Hosting.Providers; +using Telegrator.MadiatorCore; + +namespace Telegrator.Hosting +{ + public static class ServicesCollectionExtensions + { + public static IServiceCollection Configure(this IServiceCollection services, IConfiguration configuration, ConfigureOptionsProxy optionsProxy) where TOptions : class + { + optionsProxy.Configure(services, configuration); + return services; + } + + public static IServiceCollection AddTelegramBotHostDefaults(this IServiceCollection services) + { + services.AddLogging(builder => builder.AddConsole()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(services => new TelegramBotInfo(services.GetRequiredService().GetMe().Result)); + + return services; + } + + public static IServiceCollection AddTelegramReceiver(this IServiceCollection services) + { + services.AddHttpClient("tgreceiver").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory); + services.AddHostedService(); + return services; + } + + private static ITelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider) + => new TelegramBotClient(provider.GetRequiredService>().Value, httpClient); + } + + public static class TelegramBotHostExtensions + { + public static ITelegramBotHost SetBotCommands(this ITelegramBotHost botHost) + { + ITelegramBotClient client = botHost.Services.GetRequiredService(); + IEnumerable aliases = botHost.UpdateRouter.HandlersProvider.GetBotCommands(); + client.SetMyCommands(aliases).Wait(); + return botHost; + } + } +} diff --git a/Telegrator.Tests/Collections/CollectionTests.cs b/Telegrator.Tests/Collections/CollectionTests.cs new file mode 100644 index 0000000..21d661e --- /dev/null +++ b/Telegrator.Tests/Collections/CollectionTests.cs @@ -0,0 +1,317 @@ +using FluentAssertions; +using Telegram.Bot.Types.Enums; +using Telegrator.Filters.Components; +using Telegrator.MadiatorCore.Descriptors; +using Xunit; + +namespace Telegrator.Tests.Collections +{ + /// + /// Тесты для коллекций. + /// + /// ПАРАДИГМЫ ТЕСТИРОВАНИЯ: + /// 1. Collection Testing - тестирование коллекций и их операций + /// 2. List Testing - тестирование списков + /// 3. Indexing Testing - тестирование индексации + /// 4. Enumeration Testing - тестирование перечисления + /// 5. Capacity Testing - тестирование емкости коллекций + /// + public class CollectionTests + { + /// + /// Тест для HandlerDescriptorList - создание списка. + /// + /// ПРИНЦИП: Тестируем создание коллекций + /// + [Fact] + public void HandlerDescriptorList_ShouldBeCreated() + { + // Arrange & Act + var list = new HandlerDescriptorList(); + + // Assert + list.Should().NotBeNull(); + list.Should().BeEmpty(); + } + + /// + /// Тест для HandlerDescriptorList - добавление дескриптора. + /// + /// ПРИНЦИП: Тестируем добавление элементов в коллекцию + /// + [Fact] + public void HandlerDescriptorList_Add_ShouldAddDescriptor() + { + // Arrange + var list = new HandlerDescriptorList(); + var descriptor = CreateTestDescriptor(UpdateType.Message); + + // Act + list.Add(descriptor); + + // Assert + list.Should().HaveCount(1); + list.Should().Contain(descriptor); + } + + /// + /// Тест для HandlerDescriptorList - добавление нескольких дескрипторов. + /// + /// ПРИНЦИП: Тестируем множественные операции + /// + [Fact] + public void HandlerDescriptorList_AddMultiple_ShouldAddAllDescriptors() + { + // Arrange + var list = new HandlerDescriptorList(); + var descriptor1 = CreateTestDescriptor(UpdateType.Message); + var descriptor2 = CreateTestDescriptor(UpdateType.CallbackQuery); + var descriptor3 = CreateTestDescriptor(UpdateType.InlineQuery); + + // Act + list.Add(descriptor1); + list.Add(descriptor2); + list.Add(descriptor3); + + // Assert + list.Should().HaveCount(3); + list.Should().Contain(descriptor1); + list.Should().Contain(descriptor2); + list.Should().Contain(descriptor3); + } + + /// + /// Тест для HandlerDescriptorList - получение по индексу. + /// + /// ПРИНЦИП: Тестируем индексацию коллекций + /// + [Fact] + public void HandlerDescriptorList_Indexer_ShouldReturnDescriptorAtIndex() + { + // Arrange + var descriptor = CreateTestDescriptor(UpdateType.Message); + var list = new HandlerDescriptorList + { + descriptor + }; + + // Act + var result = list[0]; + + // Assert + result.Should().Be(descriptor); + } + + /// + /// Тест для HandlerDescriptorList - получение по неверному индексу. + /// + /// ПРИНЦИП: Тестируем исключения при некорректном доступе + /// + [Theory] + [InlineData(-1)] + [InlineData(1)] + [InlineData(100)] + public void HandlerDescriptorList_IndexerWithInvalidIndex_ShouldThrowArgumentOutOfRangeException(int invalidIndex) + { + // Arrange + var list = new HandlerDescriptorList + { + CreateTestDescriptor(UpdateType.Message) + }; + + // Act & Assert + list.Invoking(l => _ = l[invalidIndex]) + .Should().Throw(); + } + + /// + /// Тест для HandlerDescriptorList - перечисление элементов. + /// + /// ПРИНЦИП: Тестируем перечисление коллекций + /// + [Fact] + public void HandlerDescriptorList_ShouldBeEnumerable() + { + // Arrange + var descriptor1 = CreateTestDescriptor(UpdateType.Message); + var descriptor2 = CreateTestDescriptor(UpdateType.CallbackQuery); + var list = new HandlerDescriptorList + { + descriptor1, + descriptor2 + }; + + // Act + var enumeratedItems = list.ToList(); + + // Assert + enumeratedItems.Should().HaveCount(2); + enumeratedItems.Should().Contain(descriptor1); + enumeratedItems.Should().Contain(descriptor2); + } + + /// + /// Тест для HandlerDescriptorList - очистка списка. + /// + /// ПРИНЦИП: Тестируем очистку коллекций + /// + [Fact] + public void HandlerDescriptorList_Clear_ShouldRemoveAllDescriptors() + { + // Arrange + var list = new HandlerDescriptorList + { + CreateTestDescriptor(UpdateType.Message), + CreateTestDescriptor(UpdateType.CallbackQuery) + }; + + // Act + list.Clear(); + + // Assert + list.Should().BeEmpty(); + list.Should().HaveCount(0); + } + + /// + /// Тест для HandlerDescriptorList - проверка содержания элемента. + /// + /// ПРИНЦИП: Тестируем поиск в коллекциях + /// + [Fact] + public void HandlerDescriptorList_Contains_ShouldReturnCorrectResult() + { + // Arrange + var list = new HandlerDescriptorList(); + var descriptor = CreateTestDescriptor(UpdateType.Message); + var nonExistentDescriptor = CreateTestDescriptor(UpdateType.CallbackQuery); + + // Act + list.Add(descriptor); + var containsExisting = list.Contains(descriptor); + var containsNonExistent = list.Contains(nonExistentDescriptor); + + // Assert + containsExisting.Should().BeTrue(); + containsNonExistent.Should().BeFalse(); + } + + /// + /// Тест для HandlerDescriptorList - удаление элемента. + /// + /// ПРИНЦИП: Тестируем удаление элементов из коллекций + /// + [Fact] + public void HandlerDescriptorList_Remove_ShouldRemoveDescriptor() + { + // Arrange + var list = new HandlerDescriptorList(); + var descriptor = CreateTestDescriptor(UpdateType.Message); + list.Add(descriptor); + + // Act + var removed = list.Remove(descriptor); + + // Assert + removed.Should().BeTrue(); + list.Should().BeEmpty(); + list.Should().NotContain(descriptor); + } + + /// + /// Тест для HandlerDescriptorList - удаление несуществующего элемента. + /// + /// ПРИНЦИП: Тестируем удаление несуществующих элементов + /// + [Fact] + public void HandlerDescriptorList_RemoveNonExistent_ShouldReturnFalse() + { + // Arrange + var list = new HandlerDescriptorList(); + var nonExistentDescriptor = CreateTestDescriptor(UpdateType.CallbackQuery); + + // Act + var removed = list.Remove(nonExistentDescriptor); + + // Assert + removed.Should().BeFalse(); + list.Should().BeEmpty(); + } + + /// + /// Тест для CompletedFiltersList - создание списка. + /// + /// ПРИНЦИП: Тестируем создание специализированных коллекций + /// + [Fact] + public void CompletedFiltersList_ShouldBeCreated() + { + // Arrange & Act + var list = new CompletedFiltersList(); + + // Assert + list.Should().NotBeNull(); + list.Should().BeEmpty(); + } + + /// + /// Тест для проверки производительности коллекций. + /// + /// ПРИНЦИП: Тестируем производительность при большом количестве элементов + /// + [Fact] + public void HandlerDescriptorList_ShouldHandleLargeNumberOfItems() + { + // Arrange + var list = new HandlerDescriptorList(); + var itemsCount = 1000; + + // Act + for (int i = 0; i < itemsCount; i++) + { + list.Add(CreateTestDescriptor(UpdateType.Message)); + } + + // Assert + list.Should().HaveCount(itemsCount); + } + + /// + /// Тест для проверки потокобезопасности (базовый тест). + /// + /// ПРИНЦИП: Тестируем базовую потокобезопасность + /// + [Fact] + public async void HandlerDescriptorList_ShouldHandleConcurrentAccess() + { + // Arrange + var list = new HandlerDescriptorList(); + var tasks = new List(); + + // Act + for (int i = 0; i < 10; i++) + { + tasks.Add(Task.Run(() => + { + for (int j = 0; j < 10; j++) + { + list.Add(CreateTestDescriptor(UpdateType.Message)); + } + })); + } + + await Task.WhenAll(tasks.ToArray()); + + // Assert + list.Should().HaveCount(100); + } + + /// + /// Вспомогательный метод для создания тестового дескриптора. + /// + private static HandlerDescriptor CreateTestDescriptor(UpdateType updateType) + { + return new HandlerDescriptor(DescriptorType.General, typeof(TestUpdateHandler)); + } + } +} \ No newline at end of file diff --git a/Telegrator.Tests/Filters/FilterTests.cs b/Telegrator.Tests/Filters/FilterTests.cs new file mode 100644 index 0000000..98b8f30 --- /dev/null +++ b/Telegrator.Tests/Filters/FilterTests.cs @@ -0,0 +1,177 @@ +using FluentAssertions; +using Telegram.Bot.Types; +using Telegrator.Filters; +using Telegrator.Filters.Components; +using Xunit; + +#pragma warning disable CS8625 +namespace Telegrator.Tests.Filters +{ + /// + /// Тесты для базовых фильтров. + /// + /// ПАРАДИГМЫ ТЕСТИРОВАНИЯ: + /// 1. AAA (Arrange-Act-Assert) - структура теста: подготовка, действие, проверка + /// 2. Given-When-Then - альтернативная формулировка AAA для лучшей читаемости + /// 3. Тестирование граничных случаев и исключений + /// 4. Использование моков для изоляции тестируемого кода + /// 5. Тестирование как позитивных, так и негативных сценариев + /// + public class FilterTests + { + /// + /// Тест для AnyFilter - фильтр, который всегда проходит. + /// + /// ПРИНЦИП: Тестируем базовое поведение - фильтр должен всегда возвращать true + /// + [Fact] + public void AnyFilter_ShouldAlwaysPass() + { + // Arrange (Given) - подготовка тестовых данных + var anyFilter = Filter.Any(); + var context = new FilterExecutionContext(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary(), new CompletedFiltersList()); + + // Act (When) - выполнение тестируемого действия + var result = anyFilter.CanPass(context); + + // Assert (Then) - проверка результата + result.Should().BeTrue(); + } + + /// + /// Тест для ReverseFilter - инвертирование результата фильтра. + /// + /// ПРИНЦИП: Тестируем композицию фильтров и логику инверсии + /// + [Fact] + public void ReverseFilter_ShouldInvertResult() + { + // Arrange + var alwaysTrueFilter = Filter.Any(); + var reverseFilter = alwaysTrueFilter.Not(); + var context = new FilterExecutionContext(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary(), new CompletedFiltersList()); + + // Act + var result = reverseFilter.CanPass(context); + + // Assert + result.Should().BeFalse(); + } + + /// + /// Тест для AndFilter - логическое И между фильтрами. + /// + /// ПРИНЦИП: Тестируем комбинирование фильтров и логику И + /// + [Theory] + [InlineData(true, true, true)] // Оба фильтра проходят + [InlineData(true, false, false)] // Первый проходит, второй нет + [InlineData(false, true, false)] // Первый не проходит, второй проходит + [InlineData(false, false, false)] // Оба фильтра не проходят + public void AndFilter_ShouldCombineFiltersWithAndLogic(bool firstResult, bool secondResult, bool expectedResult) + { + // Arrange + var firstFilter = Filter.If(_ => firstResult); + var secondFilter = Filter.If(_ => secondResult); + var andFilter = firstFilter.And(secondFilter); + var context = new FilterExecutionContext(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary(), new CompletedFiltersList()); + + // Act + var result = andFilter.CanPass(context); + + // Assert + result.Should().Be(expectedResult); + } + + /// + /// Тест для OrFilter - логическое ИЛИ между фильтрами. + /// + /// ПРИНЦИП: Тестируем комбинирование фильтров и логику ИЛИ + /// + [Theory] + [InlineData(true, true, true)] // Оба фильтра проходят + [InlineData(true, false, true)] // Первый проходит, второй нет + [InlineData(false, true, true)] // Первый не проходит, второй проходит + [InlineData(false, false, false)] // Оба фильтра не проходят + public void OrFilter_ShouldCombineFiltersWithOrLogic(bool firstResult, bool secondResult, bool expectedResult) + { + // Arrange + var firstFilter = Filter.If(_ => firstResult); + var secondFilter = Filter.If(_ => secondResult); + var orFilter = firstFilter.Or(secondFilter); + var context = new FilterExecutionContext(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary(), new CompletedFiltersList()); + + // Act + var result = orFilter.CanPass(context); + + // Assert + result.Should().Be(expectedResult); + } + + /// + /// Тест для CompiledFilter - компиляция нескольких фильтров. + /// + /// ПРИНЦИП: Тестируем сложную композицию фильтров + /// + [Fact] + public void CompiledFilter_ShouldPassOnlyWhenAllFiltersPass() + { + // Arrange + var filter1 = Filter.If(_ => true); + var filter2 = Filter.If(_ => true); + var filter3 = Filter.If(_ => false); + + var compiledFilter = CompiledFilter.Compile(filter1, filter2, filter3); + var context = new FilterExecutionContext(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary(), new CompletedFiltersList()); + + // Act + var result = compiledFilter.CanPass(context); + + // Assert + result.Should().BeFalse(); // Должен вернуть false, так как filter3 возвращает false + } + + /// + /// Тест для проверки IsCollectible свойства. + /// + /// ПРИНЦИП: Тестируем свойства объектов + /// + [Fact] + public void Filter_IsCollectible_ShouldBeTrueForAnyFilter() + { + // Arrange + var anyFilter = Filter.Any(); + + // Act + var isCollectible = anyFilter.IsCollectible; + + // Assert + isCollectible.Should().BeFalse(); + } + + /// + /// Тест для FunctionFilter - фильтр на основе функции. + /// + /// ПРИНЦИП: Тестируем создание фильтров из функций + /// + [Fact] + public void FunctionFilter_ShouldUseProvidedFunction() + { + // Arrange + var wasCalled = false; + var functionFilter = Filter.If(_ => + { + wasCalled = true; + return true; + }); + var context = new FilterExecutionContext(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary(), new CompletedFiltersList()); + + // Act + var result = functionFilter.CanPass(context); + + // Assert + result.Should().BeTrue(); + wasCalled.Should().BeTrue(); + } + } +} \ No newline at end of file diff --git a/Telegrator.Tests/Handlers/HandlerTests.cs b/Telegrator.Tests/Handlers/HandlerTests.cs new file mode 100644 index 0000000..d85b92f --- /dev/null +++ b/Telegrator.Tests/Handlers/HandlerTests.cs @@ -0,0 +1,81 @@ +using FluentAssertions; +using Moq; +using Telegram.Bot.Types; +using Telegrator.Handlers; +using Xunit; + +namespace Telegrator.Tests.Handlers +{ + /// + /// Тесты для обработчиков обновлений. + /// + /// ПАРАДИГМЫ ТЕСТИРОВАНИЯ: + /// 1. Mocking - создание моков для изоляции зависимостей + /// 2. Dependency Injection - тестирование через интерфейсы + /// 3. Test Doubles - использование заглушек вместо реальных объектов + /// 4. Behavior Verification - проверка поведения, а не только результата + /// 5. Exception Testing - тестирование исключений + /// + public class HandlerTests + { + /// + /// Тест для базового обработчика обновлений. + /// + /// ПРИНЦИП: Тестируем абстрактный класс через конкретную реализацию + /// + [Fact] + public async Task UpdateHandlerBase_ShouldExecuteAndMarkLifetimeAsEnded() + { + // Arrange + var mockContainer = new Mock>(); + var testHandler = new TestUpdateHandler(); + + // Act + await testHandler.Execute(mockContainer.Object); + + // Assert + testHandler.WasExecuted.Should().BeTrue(); + testHandler.LifetimeToken.IsEnded.Should().BeTrue(); + } + + /// + /// Тест для проверки токена жизненного цикла. + /// + /// ПРИНЦИП: Тестируем состояние объектов + /// + [Fact] + public void HandlerLifetimeToken_ShouldTrackLifetimeCorrectly() + { + // Arrange + var handler = new TestUpdateHandler(); + + // Act & Assert + handler.LifetimeToken.IsEnded.Should().BeFalse(); + + // Act + handler.LifetimeToken.LifetimeEnded(); + + // Assert + handler.LifetimeToken.IsEnded.Should().BeTrue(); + } + + /// + /// Тест для проверки отмены операции. + /// + /// ПРИНЦИП: Тестируем асинхронные операции и отмену + /// + [Fact] + public async Task UpdateHandlerBase_ShouldHandleCancellation() + { + // Arrange + var mockContainer = new Mock>(); + var testHandler = new TestUpdateHandler(); + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); // Отменяем сразу + + // Act & Assert + await testHandler.Invoking(h => h.Execute(mockContainer.Object, cancellationTokenSource.Token)) + .Should().ThrowAsync(); + } + } +} \ No newline at end of file diff --git a/Telegrator.Tests/README.md b/Telegrator.Tests/README.md new file mode 100644 index 0000000..ef5dce3 --- /dev/null +++ b/Telegrator.Tests/README.md @@ -0,0 +1,232 @@ +# Тесты для Telegrator + +Этот проект содержит комплексные тесты для библиотеки Telegrator, демонстрирующие различные парадигмы и подходы к тестированию. + +## Структура тестов + +### 1. Filters (Фильтры) +**Файл:** `Filters/FilterTests.cs` + +**Парадигмы тестирования:** +- **AAA (Arrange-Act-Assert)** - структура теста: подготовка, действие, проверка +- **Given-When-Then** - альтернативная формулировка AAA для лучшей читаемости +- **Тестирование граничных случаев** и исключений +- **Использование моков** для изоляции тестируемого кода +- **Тестирование как позитивных, так и негативных сценариев** + +**Что тестируется:** +- Базовые фильтры (AnyFilter, ReverseFilter, AndFilter, OrFilter) +- Компиляция фильтров +- Логические операции между фильтрами +- Свойства фильтров (IsCollectible) + +### 2. Handlers (Обработчики) +**Файл:** `Handlers/HandlerTests.cs` + +**Парадигмы тестирования:** +- **Mocking** - создание моков для изоляции зависимостей +- **Dependency Injection** - тестирование через интерфейсы +- **Test Doubles** - использование заглушек вместо реальных объектов +- **Behavior Verification** - проверка поведения, а не только результата +- **Exception Testing** - тестирование исключений + +**Что тестируется:** +- Базовые обработчики обновлений +- Жизненный цикл обработчиков +- Обработка исключений +- Отмена операций +- Токены жизненного цикла + +### 3. Descriptors (Дескрипторы) +**Файл:** `Descriptors/HandlerDescriptorTests.cs` + +**Парадигмы тестирования:** +- **Builder Pattern Testing** - тестирование паттерна строителя +- **Factory Pattern Testing** - тестирование фабричных методов +- **Immutable Object Testing** - тестирование неизменяемых объектов +- **Configuration Testing** - тестирование конфигурации объектов +- **Validation Testing** - тестирование валидации данных + +**Что тестируется:** +- Создание дескрипторов обработчиков +- Различные типы дескрипторов (General, Singleton, Keyed, Implicit) +- Наборы фильтров +- Индексаторы +- Валидация параметров + +### 4. Providers (Провайдеры) +**Файл:** `Providers/ProviderTests.cs` + +**Парадигмы тестирования:** +- **Service Layer Testing** - тестирование сервисного слоя +- **Integration Testing** - тестирование интеграции компонентов +- **Collection Testing** - тестирование коллекций и их операций +- **Provider Pattern Testing** - тестирование паттерна провайдера +- **Dependency Resolution Testing** - тестирование разрешения зависимостей + +**Что тестируется:** +- Провайдеры обработчиков +- Коллекции обработчиков +- Операции с коллекциями +- Интеграция между провайдерами + +### 5. Hosting (Хостинг) +**Файл:** `Hosting/HostingTests.cs` + +**Парадигмы тестирования:** +- **Host Testing** - тестирование хостинга приложений +- **Configuration Testing** - тестирование конфигурации +- **Dependency Injection Testing** - тестирование DI контейнера +- **Builder Pattern Testing** - тестирование паттерна строителя +- **Lifecycle Testing** - тестирование жизненного цикла приложения + +**Что тестируется:** +- Строители хостов +- Конфигурация ботов +- Жизненный цикл хостов +- Валидация параметров + +### 6. Collections (Коллекции) +**Файл:** `Collections/CollectionTests.cs` + +**Парадигмы тестирования:** +- **Collection Testing** - тестирование коллекций и их операций +- **List Testing** - тестирование списков +- **Indexing Testing** - тестирование индексации +- **Enumeration Testing** - тестирование перечисления +- **Capacity Testing** - тестирование емкости коллекций + +**Что тестируется:** +- Списки дескрипторов обработчиков +- Списки завершенных фильтров +- Операции с коллекциями +- Производительность +- Потокобезопасность + +### 7. Integration (Интеграционные тесты) +**Файл:** `Integration/IntegrationTests.cs` + +**Парадигмы тестирования:** +- **Integration Testing** - тестирование взаимодействия компонентов +- **End-to-End Testing** - тестирование полного потока +- **System Testing** - тестирование системы в целом +- **Workflow Testing** - тестирование рабочих процессов +- **Scenario Testing** - тестирование сценариев использования + +**Что тестируется:** +- Полный цикл обработки обновлений +- Взаимодействие фильтров и обработчиков +- Композиция фильтров +- Жизненный цикл обработчиков +- Интеграция компонентов + +### 8. TestHelpers (Вспомогательные утилиты) +**Файл:** `TestHelpers/TestUtilities.cs` + +**Парадигмы тестирования:** +- **Test Utilities** - создание вспомогательных методов для тестов +- **Test Data Builders** - построители тестовых данных +- **Mock Factories** - фабрики моков +- **Test Fixtures** - фикстуры для тестов +- **Test Helpers** - вспомогательные классы для тестирования + +**Что предоставляется:** +- Утилиты для создания тестовых данных +- Фабрики моков +- Вспомогательные классы +- Тестовые обработчики + +## Основные принципы тестирования + +### 1. AAA (Arrange-Act-Assert) +```csharp +[Fact] +public void TestExample() +{ + // Arrange - подготовка тестовых данных + var filter = Filter.Any(); + var context = TestUtilities.CreateFilterContext(); + + // Act - выполнение тестируемого действия + var result = filter.CanPass(context); + + // Assert - проверка результата + result.Should().BeTrue(); +} +``` + +### 2. Тестирование граничных случаев +```csharp +[Theory] +[InlineData(-1)] +[InlineData(1)] +[InlineData(100)] +public void TestBoundaryConditions(int invalidIndex) +{ + // Тестируем граничные случаи +} +``` + +### 3. Использование моков +```csharp +[Fact] +public void TestWithMocks() +{ + // Arrange + var mockClient = new Mock(); + var mockContainer = TestUtilities.CreateMockHandlerContainer(); + + // Act & Assert + // Тестирование с моками +} +``` + +### 4. Тестирование исключений +```csharp +[Fact] +public void TestExceptions() +{ + // Act & Assert + Action action = () => { /* код, который должен выбросить исключение */ }; + action.Should().Throw(); +} +``` + +## Запуск тестов + +### Через командную строку +```bash +dotnet test +``` + +### Через Visual Studio +1. Откройте Test Explorer +2. Запустите все тесты или выберите конкретные + +### Через Rider +1. Откройте Unit Tests window +2. Запустите тесты + +## Покрытие кода + +Для анализа покрытия кода используйте: +```bash +dotnet test --collect:"XPlat Code Coverage" +``` + +## Рекомендации по написанию тестов + +1. **Именование тестов** должно быть описательным и следовать паттерну `MethodName_Scenario_ExpectedResult` +2. **Каждый тест** должен тестировать только одну вещь +3. **Используйте моки** для изоляции зависимостей +4. **Тестируйте как позитивные, так и негативные сценарии** +5. **Группируйте связанные тесты** в отдельные классы +6. **Используйте вспомогательные методы** для создания тестовых данных +7. **Документируйте сложные тесты** с помощью комментариев + +## Полезные ссылки + +- [xUnit Documentation](https://xunit.net/) +- [Moq Documentation](https://github.com/moq/moq4) +- [FluentAssertions Documentation](https://fluentassertions.com/) +- [.NET Testing Best Practices](https://docs.microsoft.com/en-us/dotnet/core/testing/) \ No newline at end of file diff --git a/Telegrator.Tests/Telegrator.Tests.csproj b/Telegrator.Tests/Telegrator.Tests.csproj new file mode 100644 index 0000000..2a11e6c --- /dev/null +++ b/Telegrator.Tests/Telegrator.Tests.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + enable + enable + Debug;Release + false + + + + + + + + + + + + + + + + + diff --git a/Telegrator.Tests/TestUpdateHandler.cs b/Telegrator.Tests/TestUpdateHandler.cs new file mode 100644 index 0000000..6248edf --- /dev/null +++ b/Telegrator.Tests/TestUpdateHandler.cs @@ -0,0 +1,23 @@ +using Telegram.Bot.Types; +using Telegrator.Handlers; + +namespace Telegrator.Tests +{ + /// + /// Вспомогательный класс для тестирования абстрактного UpdateHandlerBase. + /// + /// ПРИНЦИП: Создание тестовых двойников для абстрактных классов + /// + [MessageHandler] + internal class TestUpdateHandler : MessageHandler + { + public bool WasExecuted { get; private set; } + + public override Task Execute(IAbstractHandlerContainer container, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + WasExecuted = true; + return Task.CompletedTask; + } + } +} diff --git a/Telegrator.sln b/Telegrator.sln new file mode 100644 index 0000000..0f9d08f --- /dev/null +++ b/Telegrator.sln @@ -0,0 +1,65 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36119.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegrator", "Telegrator\Telegrator.csproj", "{12D1D209-6473-4F58-BD66-846F0D85F6FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegrator.Hosting", "Telegrator.Hosting\Telegrator.Hosting.csproj", "{C4C25E5F-5B8D-4E87-B474-DBAFD70BE1E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegrator.Tests", "Telegrator.Tests\Telegrator.Tests.csproj", "{0926C71D-FE0C-4963-B08B-1CBAFF1E3276}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegrator.Generators", "Telegrator.Generators\Telegrator.Generators.csproj", "{43927959-EB6D-4CBA-A652-2B7FC0C1DDA7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegrator.Analyzers", "Telegrator.Analyzers\Telegrator.Analyzers.csproj", "{8B6A32EA-ECF7-4CAB-A1E5-2392063C986D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + AnalyzersDebug|Any CPU = AnalyzersDebug|Any CPU + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {12D1D209-6473-4F58-BD66-846F0D85F6FD}.AnalyzersDebug|Any CPU.ActiveCfg = AnalyzersDebug|Any CPU + {12D1D209-6473-4F58-BD66-846F0D85F6FD}.AnalyzersDebug|Any CPU.Build.0 = AnalyzersDebug|Any CPU + {12D1D209-6473-4F58-BD66-846F0D85F6FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12D1D209-6473-4F58-BD66-846F0D85F6FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12D1D209-6473-4F58-BD66-846F0D85F6FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12D1D209-6473-4F58-BD66-846F0D85F6FD}.Release|Any CPU.Build.0 = Release|Any CPU + {C4C25E5F-5B8D-4E87-B474-DBAFD70BE1E1}.AnalyzersDebug|Any CPU.ActiveCfg = AnalyzersDebug|Any CPU + {C4C25E5F-5B8D-4E87-B474-DBAFD70BE1E1}.AnalyzersDebug|Any CPU.Build.0 = AnalyzersDebug|Any CPU + {C4C25E5F-5B8D-4E87-B474-DBAFD70BE1E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4C25E5F-5B8D-4E87-B474-DBAFD70BE1E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4C25E5F-5B8D-4E87-B474-DBAFD70BE1E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4C25E5F-5B8D-4E87-B474-DBAFD70BE1E1}.Release|Any CPU.Build.0 = Release|Any CPU + {0926C71D-FE0C-4963-B08B-1CBAFF1E3276}.AnalyzersDebug|Any CPU.ActiveCfg = AnalyzersDebug|Any CPU + {0926C71D-FE0C-4963-B08B-1CBAFF1E3276}.AnalyzersDebug|Any CPU.Build.0 = AnalyzersDebug|Any CPU + {0926C71D-FE0C-4963-B08B-1CBAFF1E3276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0926C71D-FE0C-4963-B08B-1CBAFF1E3276}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0926C71D-FE0C-4963-B08B-1CBAFF1E3276}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0926C71D-FE0C-4963-B08B-1CBAFF1E3276}.Release|Any CPU.Build.0 = Release|Any CPU + {43927959-EB6D-4CBA-A652-2B7FC0C1DDA7}.AnalyzersDebug|Any CPU.ActiveCfg = AnalyzersDebug|Any CPU + {43927959-EB6D-4CBA-A652-2B7FC0C1DDA7}.AnalyzersDebug|Any CPU.Build.0 = AnalyzersDebug|Any CPU + {43927959-EB6D-4CBA-A652-2B7FC0C1DDA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43927959-EB6D-4CBA-A652-2B7FC0C1DDA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43927959-EB6D-4CBA-A652-2B7FC0C1DDA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43927959-EB6D-4CBA-A652-2B7FC0C1DDA7}.Release|Any CPU.Build.0 = Release|Any CPU + {8B6A32EA-ECF7-4CAB-A1E5-2392063C986D}.AnalyzersDebug|Any CPU.ActiveCfg = Release|Any CPU + {8B6A32EA-ECF7-4CAB-A1E5-2392063C986D}.AnalyzersDebug|Any CPU.Build.0 = Release|Any CPU + {8B6A32EA-ECF7-4CAB-A1E5-2392063C986D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B6A32EA-ECF7-4CAB-A1E5-2392063C986D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B6A32EA-ECF7-4CAB-A1E5-2392063C986D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B6A32EA-ECF7-4CAB-A1E5-2392063C986D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {951FEB3F-5B9A-4731-A828-3486D9FE4939} + EndGlobalSection +EndGlobal diff --git a/Telegrator/Annotations/CommandAlliasAttribute.cs b/Telegrator/Annotations/CommandAlliasAttribute.cs new file mode 100644 index 0000000..bf45781 --- /dev/null +++ b/Telegrator/Annotations/CommandAlliasAttribute.cs @@ -0,0 +1,59 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; +using Telegrator.Attributes; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages based on command aliases. + /// Allows handlers to respond to multiple command variations using a single attribute. + /// + public class CommandAlliasAttribute : UpdateFilterAttribute + { + /// + /// Gets the allowed update types for this filter. + /// + public override UpdateType[] AllowedTypes => [UpdateType.Message]; + + /// + /// The description of the command (defaults to "no description provided"). + /// + private string _description = "no description provided"; + + /// + /// Gets the array of command aliases that this filter will match. + /// + public string[] Alliases + { + get; + private set; + } + + /// + /// Gets or sets the description of the command. + /// Must be between 0 and 256 characters in length. + /// + /// Thrown when the description length is outside the allowed range. + public string Description + { + get => _description; + set => _description = value is { Length: <= 256 and >= 0 } + ? value : throw new ArgumentOutOfRangeException(nameof(value)); + } + + /// + /// Initializes a new instance of the CommandAlliasAttribute with the specified command aliases. + /// + /// The command aliases to match against. + public CommandAlliasAttribute(params string[] alliases) + : base(new CommandAlliasFilter(alliases)) => Alliases = alliases; + + /// + /// Gets the filtering target (Message) from the update. + /// + /// The Telegram update. + /// The message from the update, or null if not present. + public override Message? GetFilterringTarget(Update update) => update.Message; + } +} diff --git a/Telegrator/Annotations/EnvironmentFilterAttributes.cs b/Telegrator/Annotations/EnvironmentFilterAttributes.cs new file mode 100644 index 0000000..f583cc5 --- /dev/null +++ b/Telegrator/Annotations/EnvironmentFilterAttributes.cs @@ -0,0 +1,85 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; +using Telegrator.Attributes; +using Telegrator.Filters.Components; + +namespace Telegrator.Annotations +{ + /// + /// Abstract base attribute for filtering updates based on environment conditions. + /// Can process all types of updates and provides environment-specific filtering logic. + /// + /// The environment filters to apply + public abstract class EnvironmentFilterAttribute(params IFilter[] filters) : UpdateFilterAttribute(filters) + { + /// + /// Gets the allowed update types that this filter can process. + /// Environment filters can process all update types. + /// + public override UpdateType[] AllowedTypes => Update.AllTypes; + + /// + /// Gets the update as the filtering target. + /// Environment filters work with the entire update object. + /// + /// The Telegram update + /// The update object itself + public override Update? GetFilterringTarget(Update update) + => update; + } + + /// + /// Attribute for filtering updates that occur in debug environment. + /// Only allows updates when the application is running in debug mode. + /// + public class IsDebugEnvironmentAttribute() + : EnvironmentFilterAttribute(new IsDebugEnvironmentFilter()) + { } + + /// + /// Attribute for filtering updates that occur in release environment. + /// Only allows updates when the application is running in release mode. + /// + public class IsReleaseEnvironmentAttribute() + : EnvironmentFilterAttribute(new IsReleaseEnvironmentFilter()) + { } + + /// + /// Attribute for filtering updates based on environment variable values. + /// + public class EnvironmentVariableAttribute : EnvironmentFilterAttribute + { + /// + /// Initializes the attribute to filter based on an environment variable with a specific value and comparison method. + /// + /// The name of the environment variable + /// The expected value of the environment variable + /// The string comparison method + public EnvironmentVariableAttribute(string variable, string? value, StringComparison comparison) + : base(new EnvironmentVariableFilter(variable, value, comparison)) { } + + /// + /// Initializes the attribute to filter based on an environment variable with a specific value. + /// + /// The name of the environment variable + /// The expected value of the environment variable + public EnvironmentVariableAttribute(string variable, string? value) + : base(new EnvironmentVariableFilter(variable, value)) { } + + /// + /// Initializes the attribute to filter based on the existence of an environment variable. + /// + /// The name of the environment variable + public EnvironmentVariableAttribute(string variable) + : base(new EnvironmentVariableFilter(variable)) { } + + /// + /// Initializes the attribute to filter based on an environment variable with a specific comparison method. + /// + /// The name of the environment variable + /// The string comparison method + public EnvironmentVariableAttribute(string variable, StringComparison comparison) + : base(new EnvironmentVariableFilter(variable, comparison)) { } + } +} diff --git a/Telegrator/Annotations/MentionedAttribute.cs b/Telegrator/Annotations/MentionedAttribute.cs new file mode 100644 index 0000000..e70f3d4 --- /dev/null +++ b/Telegrator/Annotations/MentionedAttribute.cs @@ -0,0 +1,40 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages that contain mentions. + /// Allows handlers to respond only to messages that mention the bot or specific users. + /// + public class MentionedAttribute : MessageFilterAttribute + { + /// + /// Initializes a new instance of the MentionedAttribute that matches any mention. + /// + public MentionedAttribute() + : base(new MessageHasEntityFilter(MessageEntityType.Mention, 0, null), new MentionedFilter()) { } + + /// + /// Initializes a new instance of the MentionedAttribute that matches mentions at a specific offset. + /// + /// The offset position where the mention should occur. + public MentionedAttribute(int offset) + : base(new MessageHasEntityFilter(MessageEntityType.Mention, offset, null), new MentionedFilter()) { } + + /// + /// Initializes a new instance of the MentionedAttribute that matches a specific mention. + /// + /// The specific mention text to match. + public MentionedAttribute(string mention) + : base(new MessageHasEntityFilter(MessageEntityType.Mention), new MentionedFilter(mention)) { } + + /// + /// Initializes a new instance of the MentionedAttribute that matches a specific mention at a specific offset. + /// + /// The specific mention text to match. + /// The offset position where the mention should occur. + public MentionedAttribute(string mention, int offset) + : base(new MessageHasEntityFilter(MessageEntityType.Mention, offset, null), new MentionedFilter(mention)) { } + } +} diff --git a/Telegrator/Annotations/MessageChatFilterAttributes.cs b/Telegrator/Annotations/MessageChatFilterAttributes.cs new file mode 100644 index 0000000..ccb3620 --- /dev/null +++ b/Telegrator/Annotations/MessageChatFilterAttributes.cs @@ -0,0 +1,93 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages sent in forum chats. + /// + public class ChatIsForumAttribute() + : MessageFilterAttribute(new MessageChatIsForumFilter()) + { } + + /// + /// Attribute for filtering messages sent in a specific chat by ID. + /// + /// The chat ID to match + public class ChatIdAttribute(long id) + : MessageFilterAttribute(new MessageChatIdFilter(id)) + { } + + /// + /// Attribute for filtering messages sent in chats of a specific type. + /// + /// The chat type to match + public class ChatTypeAttribute(ChatType type) + : MessageFilterAttribute(new MessageChatTypeFilter(type)) + { } + + /// + /// Attribute for filtering messages based on the chat title. + /// + public class ChatTitleAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages from chats with a specific title and comparison method. + /// + /// The chat title to match + /// The string comparison method + public ChatTitleAttribute(string? title, StringComparison comparison) + : base(new MessageChatTitleFilter(title, comparison)) { } + + /// + /// Initializes the attribute to filter messages from chats with a specific title. + /// + /// The chat title to match + public ChatTitleAttribute(string? title) + : base(new MessageChatTitleFilter(title)) { } + } + + /// + /// Attribute for filtering messages based on the chat username. + /// + public class ChatUsernameAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages from chats with a specific username and comparison method. + /// + /// The chat username to match + /// The string comparison method + public ChatUsernameAttribute(string? userName, StringComparison comparison) + : base(new MessageChatUsernameFilter(userName, comparison)) { } + + /// + /// Initializes the attribute to filter messages from chats with a specific username. + /// + /// The chat username to match + public ChatUsernameAttribute(string? userName) + : base(new MessageChatUsernameFilter(userName, StringComparison.InvariantCulture)) { } + } + + /// + /// Attribute for filtering messages based on the chat name (first name and optionally last name). + /// + public class ChatNameAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages from chats with specific first and last names. + /// + /// The first name to match + /// The last name to match (optional) + /// The string comparison method + public ChatNameAttribute(string? firstName, string? lastName, StringComparison comparison) + : base(new MessageChatNameFilter(firstName, lastName, comparison)) { } + + /// + /// Initializes the attribute to filter messages from chats with specific first and last names. + /// + /// The first name to match + /// The last name to match (optional) + public ChatNameAttribute(string? firstName, string? lastName) + : base(new MessageChatNameFilter(firstName, lastName)) { } + } +} diff --git a/Telegrator/Annotations/MessageFilterAttributes.cs b/Telegrator/Annotations/MessageFilterAttributes.cs new file mode 100644 index 0000000..0b812bb --- /dev/null +++ b/Telegrator/Annotations/MessageFilterAttributes.cs @@ -0,0 +1,161 @@ +using System.Text.RegularExpressions; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; +using Telegrator.Attributes; +using Telegrator.Filters.Components; + +namespace Telegrator.Annotations +{ + /// + /// Abstract base attribute for filtering message-based updates. + /// Supports various message types including regular messages, edited messages, channel posts, and business messages. + /// + /// The filters to apply to messages + public abstract class MessageFilterAttribute(params IFilter[] filters) : UpdateFilterAttribute(filters) + { + /// + /// Gets the allowed update types that this filter can process. + /// + public override UpdateType[] AllowedTypes => + [ + UpdateType.Message, + UpdateType.EditedMessage, + UpdateType.ChannelPost, + UpdateType.EditedChannelPost, + UpdateType.BusinessMessage, + UpdateType.EditedBusinessMessage + ]; + + /// + /// Extracts the message from various types of updates. + /// + /// The Telegram update + /// The message from the update, or null if not present + public override Message? GetFilterringTarget(Update update) + { + return update switch + { + { Message: { } message } => message, + { EditedMessage: { } message } => message, + { ChannelPost: { } message } => message, + { EditedChannelPost: { } message } => message, + { BusinessMessage: { } message } => message, + { EditedBusinessMessage: { } message } => message, + _ => null + }; + } + } + + /// + /// Attribute for filtering messages based on regular expression patterns. + /// + public class MessageRegexAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute with a regex pattern and options. + /// + /// The regular expression pattern to match + /// The regex options for matching + public MessageRegexAttribute(string pattern, RegexOptions regexOptions = default) + : base(new MessageRegexFilter(pattern, regexOptions)) { } + + /// + /// Initializes the attribute with a precompiled regex. + /// + /// The precompiled regular expression + public MessageRegexAttribute(Regex regex) + : base(new MessageRegexFilter(regex)) { } + } + + /// + /// Attribute for filtering messages that contain dice throws with specific values. + /// + public class DiceThrowedAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter dice throws with a specific value. + /// + /// The dice value to match + public DiceThrowedAttribute(int value) + : base(new DiceThrowedFilter(value)) { } + + /// + /// Initializes the attribute to filter dice throws with a specific type and value. + /// + /// The type of dice + /// The dice value to match + public DiceThrowedAttribute(DiceType diceType, int value) + : base(new DiceThrowedFilter(diceType, value)) { } + } + + /// + /// Attribute for filtering messages that are automatically forwarded. + /// + public class IsAutomaticFormwardMessageAttribute() + : MessageFilterAttribute(new IsAutomaticFormwardMessageFilter()) + { } + + /// + /// Attribute for filtering messages sent while the user was offline. + /// + public class IsFromOfflineMessageAttribute() + : MessageFilterAttribute(new IsFromOfflineMessageFilter()) + { } + + /// + /// Attribute for filtering service messages (e.g., user joined, left, etc.). + /// + public class IsServiceMessageMessageAttribute() + : MessageFilterAttribute(new IsServiceMessageMessageFilter()) + { } + + /// + /// Attribute for filtering topic messages in forum chats. + /// + public class IsTopicMessageMessageAttribute() + : MessageFilterAttribute(new IsServiceMessageMessageFilter()) + { } + + /// + /// Attribute for filtering messages based on their entities (mentions, links, etc.). + /// + public class MessageHasEntityAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages with a specific entity type. + /// + /// The entity type to match + public MessageHasEntityAttribute(MessageEntityType type) + : base(new MessageHasEntityFilter(type)) { } + + /// + /// Initializes the attribute to filter messages with a specific entity type at a specific position. + /// + /// The entity type to match + /// The starting position of the entity + /// The length of the entity (optional) + public MessageHasEntityAttribute(MessageEntityType type, int offset, int? length) + : base(new MessageHasEntityFilter(type, offset, length)) { } + + /// + /// Initializes the attribute to filter messages with a specific entity type and content. + /// + /// The entity type to match + /// The content that the entity should contain + /// The string comparison method + public MessageHasEntityAttribute(MessageEntityType type, string content, StringComparison stringComparison = StringComparison.CurrentCulture) + : base(new MessageHasEntityFilter(type, content, stringComparison)) { } + + /// + /// Initializes the attribute to filter messages with a specific entity type, position, and content. + /// + /// The entity type to match + /// The starting position of the entity + /// The length of the entity (optional) + /// The content that the entity should contain + /// The string comparison method + public MessageHasEntityAttribute(MessageEntityType type, int offset, int? length, string content, StringComparison stringComparison = StringComparison.CurrentCulture) + : base(new MessageHasEntityFilter(type, offset, length, content, stringComparison)) { } + } +} diff --git a/Telegrator/Annotations/MessageSenderFilterAttributes.cs b/Telegrator/Annotations/MessageSenderFilterAttributes.cs new file mode 100644 index 0000000..40ba586 --- /dev/null +++ b/Telegrator/Annotations/MessageSenderFilterAttributes.cs @@ -0,0 +1,92 @@ +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages based on the sender's username. + /// + public class FromUsernameAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages from a specific username. + /// + /// The username to match + public FromUsernameAttribute(string username) + : base(new FromUsernameFilter(username)) { } + + /// + /// Initializes the attribute to filter messages from a specific username with custom comparison. + /// + /// The username to match + /// The string comparison method + public FromUsernameAttribute(string username, StringComparison comparison) + : base(new FromUsernameFilter(username, comparison)) { } + } + + /// + /// Attribute for filtering messages based on the sender's name (first name and optionally last name). + /// + public class FromUserAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages from a user with specific first and last names. + /// + /// The first name to match + /// The last name to match (optional) + /// The string comparison method + public FromUserAttribute(string firstName, string? lastName, StringComparison comparison) + : base(new FromUserFilter(firstName, lastName, comparison)) { } + + /// + /// Initializes the attribute to filter messages from a user with specific first and last names. + /// + /// The first name to match + /// The last name to match + public FromUserAttribute(string firstName, string? lastName) + : base(new FromUserFilter(firstName, lastName, StringComparison.InvariantCulture)) { } + + /// + /// Initializes the attribute to filter messages from a user with a specific first name. + /// + /// The first name to match + public FromUserAttribute(string firstName) + : base(new FromUserFilter(firstName, null, StringComparison.InvariantCulture)) { } + + /// + /// Initializes the attribute to filter messages from a user with a specific first name and custom comparison. + /// + /// The first name to match + /// The string comparison method + public FromUserAttribute(string firstName, StringComparison comparison) + : base(new FromUserFilter(firstName, null, comparison)) { } + } + + /// + /// Attribute for filtering messages from a specific user ID. + /// + /// The user ID to match + public class FromUserIdAttribute(long userId) + : MessageFilterAttribute(new FromUserIdFilter(userId)) + { } + + /// + /// Attribute for filtering messages sent by not bots (users). + /// + public class NotFromBotAttribute() + : MessageFilterAttribute(new FromBotFilter().Not()) + { } + + /// + /// Attribute for filtering messages sent by bots. + /// + public class FromBotAttribute() + : MessageFilterAttribute(new FromBotFilter()) + { } + + /// + /// Attribute for filtering messages sent by premium users. + /// + public class FromPremiumUserAttribute() + : MessageFilterAttribute(new FromPremiumUserFilter()) + { } +} diff --git a/Telegrator/Annotations/MessageTextFilterAttributes.cs b/Telegrator/Annotations/MessageTextFilterAttributes.cs new file mode 100644 index 0000000..c73affe --- /dev/null +++ b/Telegrator/Annotations/MessageTextFilterAttributes.cs @@ -0,0 +1,47 @@ +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages where the text starts with the specified content. + /// + /// The string that the message text should start with + /// The string comparison type + public class TextStartsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture) + : MessageFilterAttribute(new TextStartsWithFilter(content, comparison)) + { } + + /// + /// Attribute for filtering messages where the text ends with the specified content. + /// + /// The string that the message text should end with + /// The string comparison type + public class TextEndsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture) + : MessageFilterAttribute(new TextEndsWithFilter(content, comparison)) + { } + + /// + /// Attribute for filtering messages where the text contains the specified content. + /// + /// The string that the message text should contain + /// The string comparison type + public class TextContainsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture) + : MessageFilterAttribute(new TextContainsFilter(content, comparison)) + { } + + /// + /// Attribute for filtering messages where the text equals the specified content. + /// + /// The string that the message text should equal + /// The string comparison type + public class TextEqualsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture) + : MessageFilterAttribute(new TextEqualsFilter(content, comparison)) + { } + + /// + /// Attribute for filtering messages that contain any non-empty text. + /// + public class HasTextAttribute() + : MessageFilterAttribute(new TextNotNullOrEmptyFilter()) + { } +} diff --git a/Telegrator/Annotations/RepliedMentionedAttribute.cs b/Telegrator/Annotations/RepliedMentionedAttribute.cs new file mode 100644 index 0000000..1dfdba3 --- /dev/null +++ b/Telegrator/Annotations/RepliedMentionedAttribute.cs @@ -0,0 +1,44 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages that are replies to messages containing mentions. + /// Allows handlers to respond to messages that reply to messages with specific mentions. + /// + public class RepliedMentionedAttribute : MessageFilterAttribute + { + /// + /// Initializes a new instance of the RepliedMentionedAttribute that matches replies to any mention. + /// + /// The depth of the reply chain to check (default: 1). + public RepliedMentionedAttribute(int replyDepth = 1) + : base(new RepliedMessageHasEntityFilter(MessageEntityType.Mention, 0, null, replyDepth), new RepliedMentionedFilter(replyDepth)) { } + + /// + /// Initializes a new instance of the RepliedMentionedAttribute that matches replies to mentions at a specific offset. + /// + /// The offset position where the mention should occur in the replied message. + /// The depth of the reply chain to check (default: 1). + public RepliedMentionedAttribute(int offset, int replyDepth = 1) + : base(new RepliedMessageHasEntityFilter(MessageEntityType.Mention, offset, null, replyDepth), new RepliedMentionedFilter(replyDepth)) { } + + /// + /// Initializes a new instance of the RepliedMentionedAttribute that matches replies to a specific mention. + /// + /// The specific mention text to match in the replied message. + /// The depth of the reply chain to check (default: 1). + public RepliedMentionedAttribute(string mention, int replyDepth = 1) + : base(new RepliedMessageHasEntityFilter(MessageEntityType.Mention, replyDepth), new RepliedMentionedFilter(mention, replyDepth)) { } + + /// + /// Initializes a new instance of the RepliedMentionedAttribute that matches replies to a specific mention at a specific offset. + /// + /// The specific mention text to match in the replied message. + /// The offset position where the mention should occur in the replied message. + /// The depth of the reply chain to check (default: 1). + public RepliedMentionedAttribute(string mention, int offset, int replyDepth = 1) + : base(new RepliedMessageHasEntityFilter(MessageEntityType.Mention, offset, null, replyDepth), new RepliedMentionedFilter(mention, replyDepth)) { } + } +} diff --git a/Telegrator/Annotations/RepliedMessageChatFilterAttributes.cs b/Telegrator/Annotations/RepliedMessageChatFilterAttributes.cs new file mode 100644 index 0000000..98168e6 --- /dev/null +++ b/Telegrator/Annotations/RepliedMessageChatFilterAttributes.cs @@ -0,0 +1,102 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages where the replied-to message was sent in a forum chat. + /// + /// How many levels up the reply chain to check (default: 1) + public class RepliedChatIsForumAttribute(int replyDepth = 1) + : MessageFilterAttribute(new RepliedMessageChatIsForumFilter(replyDepth)) + { } + + /// + /// Attribute for filtering messages where the replied-to message was sent in a specific chat by ID. + /// + /// The chat ID to match + /// How many levels up the reply chain to check (default: 1) + public class RepliedChatIdAttribute(long id, int replyDepth = 1) + : MessageFilterAttribute(new RepliedMessageChatIdFilter(id, replyDepth)) + { } + + /// + /// Attribute for filtering messages where the replied-to message was sent in a chat of a specific type. + /// + /// The chat type to match + /// How many levels up the reply chain to check (default: 1) + public class RepliedChatTypeAttribute(ChatType type, int replyDepth = 1) + : MessageFilterAttribute(new RepliedMessageChatTypeFilter(type, replyDepth)) + { } + + /// + /// Attribute for filtering messages based on the chat title of the replied-to message. + /// + public class RepliedChatTitleAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages where the replied-to message is from a chat with a specific title and comparison method. + /// + /// The chat title to match + /// The string comparison method + /// How many levels up the reply chain to check (default: 1) + public RepliedChatTitleAttribute(string? title, StringComparison comparison, int replyDepth = 1) + : base(new RepliedMessageChatTitleFilter(title, comparison, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message is from a chat with a specific title. + /// + /// The chat title to match + /// How many levels up the reply chain to check (default: 1) + public RepliedChatTitleAttribute(string? title, int replyDepth = 1) + : base(new RepliedMessageChatTitleFilter(title, StringComparison.InvariantCulture, replyDepth)) { } + } + + /// + /// Attribute for filtering messages based on the chat username of the replied-to message. + /// + public class RepliedChatUsernameAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages where the replied-to message is from a chat with a specific username and comparison method. + /// + /// The chat username to match + /// The string comparison method + /// How many levels up the reply chain to check (default: 1) + public RepliedChatUsernameAttribute(string? userName, StringComparison comparison, int replyDepth = 1) + : base(new RepliedMessageChatUsernameFilter(userName, comparison, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message is from a chat with a specific username. + /// + /// The chat username to match + /// How many levels up the reply chain to check (default: 1) + public RepliedChatUsernameAttribute(string? userName, int replyDepth = 1) + : base(new RepliedMessageChatUsernameFilter(userName, replyDepth)) { } + } + + /// + /// Attribute for filtering messages based on the chat name of the replied-to message. + /// + public class RepliedChatNameAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages where the replied-to message is from a chat with specific first and last names. + /// + /// The first name to match + /// The last name to match (optional) + /// The string comparison method + /// How many levels up the reply chain to check (default: 1) + public RepliedChatNameAttribute(string? firstName, string? lastName, StringComparison comparison, int replyDepth = 1) + : base(new RepliedMessageChatNameFilter(firstName, lastName, comparison, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message is from a chat with specific first and last names. + /// + /// The first name to match + /// The last name to match (optional) + /// How many levels up the reply chain to check (default: 1) + public RepliedChatNameAttribute(string? firstName, string? lastName, int replyDepth = 1) + : base(new RepliedMessageChatNameFilter(firstName, lastName, StringComparison.InvariantCulture, replyDepth)) { } + } +} diff --git a/Telegrator/Annotations/RepliedMessageFilterAttributes.cs b/Telegrator/Annotations/RepliedMessageFilterAttributes.cs new file mode 100644 index 0000000..284d3a7 --- /dev/null +++ b/Telegrator/Annotations/RepliedMessageFilterAttributes.cs @@ -0,0 +1,114 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages that are replies to other messages. + /// + /// How many levels up the reply chain to check (default: 1) + public class MessageRepliedAttribute(int replyDepth = 1) + : MessageFilterAttribute(new MessageRepliedFilter(replyDepth)) + { } + + /// + /// Attribute for filtering messages where the replied-to message contains dice throws with specific values. + /// + public class RepliedDiceThrowedAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages where the replied-to message contains a dice throw with a specific value. + /// + /// The dice value to match + /// How many levels up the reply chain to check (default: 1) + public RepliedDiceThrowedAttribute(int value, int replyDepth = 1) + : base(new RepliedDiceThrowedFilter(value, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message contains a dice throw with a specific type and value. + /// + /// The type of dice + /// The dice value to match + /// How many levels up the reply chain to check (default: 1) + public RepliedDiceThrowedAttribute(DiceType diceType, int value, int replyDepth = 1) + : base(new RepliedDiceThrowedFilter(diceType, value, replyDepth)) { } + } + + /// + /// Attribute for filtering messages where the replied-to message was automatically forwarded. + /// + /// How many levels up the reply chain to check (default: 1) + public class RepliedIsAutomaticFormwardMessageAttribute(int replyDepth = 1) + : MessageFilterAttribute(new RepliedIsAutomaticFormwardMessageFilter(replyDepth)) + { } + + /// + /// Attribute for filtering messages where the replied-to message was sent while the user was offline. + /// + /// How many levels up the reply chain to check (default: 1) + public class RepliedIsFromOfflineMessageAttribute(int replyDepth = 1) + : MessageFilterAttribute(new RepliedIsFromOfflineMessageFilter(replyDepth)) + { } + + /// + /// Attribute for filtering messages where the replied-to message is a service message. + /// + /// How many levels up the reply chain to check (default: 1) + public class RepliedIsServiceMessageMessageAttribute(int replyDepth = 1) + : MessageFilterAttribute(new RepliedIsServiceMessageMessageFilter(replyDepth)) + { } + + /// + /// Attribute for filtering messages where the replied-to message is a topic message in forum chats. + /// + /// How many levels up the reply chain to check (default: 1) + public class RepliedIsTopicMessageMessageAttribut(int replyDepth = 1) + : MessageFilterAttribute(new RepliedIsServiceMessageMessageFilter(replyDepth)) + { } + + /// + /// Attribute for filtering messages based on entities in the replied-to message. + /// + public class RepliedMessageHasEntityAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages where the replied-to message has a specific entity type. + /// + /// The entity type to match + /// How many levels up the reply chain to check (default: 1) + public RepliedMessageHasEntityAttribute(MessageEntityType type, int replyDepth = 1) + : base(new RepliedMessageHasEntityFilter(type, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message has a specific entity type at a specific position. + /// + /// The entity type to match + /// The starting position of the entity + /// The length of the entity (optional) + /// How many levels up the reply chain to check (default: 1) + public RepliedMessageHasEntityAttribute(MessageEntityType type, int offset, int? length, int replyDepth = 1) + : base(new RepliedMessageHasEntityFilter(type, offset, length, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message has a specific entity type with specific content. + /// + /// The entity type to match + /// The content that the entity should contain + /// The string comparison method + /// How many levels up the reply chain to check (default: 1) + public RepliedMessageHasEntityAttribute(MessageEntityType type, string content, StringComparison stringComparison = StringComparison.CurrentCulture, int replyDepth = 1) + : base(new RepliedMessageHasEntityFilter(type, content, stringComparison, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message has a specific entity type at a specific position with specific content. + /// + /// The entity type to match + /// The starting position of the entity + /// The length of the entity (optional) + /// The content that the entity should contain + /// The string comparison method + /// How many levels up the reply chain to check (default: 1) + public RepliedMessageHasEntityAttribute(MessageEntityType type, int offset, int? length, string content, StringComparison stringComparison = StringComparison.CurrentCulture, int replyDepth = 1) + : base(new RepliedMessageHasEntityFilter(type, offset, length, content, stringComparison, replyDepth)) { } + } +} diff --git a/Telegrator/Annotations/RepliedMessageSenderFilterAttributes.cs b/Telegrator/Annotations/RepliedMessageSenderFilterAttributes.cs new file mode 100644 index 0000000..ee22e48 --- /dev/null +++ b/Telegrator/Annotations/RepliedMessageSenderFilterAttributes.cs @@ -0,0 +1,94 @@ +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages based on the username of the sender of the replied-to message. + /// + public class RepliedFromUsernameAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages where the replied-to message is from a specific username. + /// + /// The username to match + /// How many levels up the reply chain to check (default: 1) + public RepliedFromUsernameAttribute(string username, int replyDepth = 1) + : base(new RepliedUsernameFilter(username, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message is from a specific username with custom comparison. + /// + /// The username to match + /// The string comparison method + /// How many levels up the reply chain to check (default: 1) + public RepliedFromUsernameAttribute(string username, StringComparison comparison, int replyDepth = 1) + : base(new RepliedUsernameFilter(username, comparison, replyDepth)) { } + } + + /// + /// Attribute for filtering messages based on the name of the sender of the replied-to message. + /// + public class RepliedFromUserAttribute : MessageFilterAttribute + { + /// + /// Initializes the attribute to filter messages where the replied-to message is from a user with specific names. + /// + /// The first name to match + /// The last name to match (optional) + /// The string comparison method + /// How many levels up the reply chain to check (default: 1) + public RepliedFromUserAttribute(string firstName, string? lastName, StringComparison comparison, int replyDepth = 1) + : base(new RepliedUserFilter(firstName, lastName, comparison, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message is from a user with specific names. + /// + /// The first name to match + /// The last name to match + /// How many levels up the reply chain to check (default: 1) + public RepliedFromUserAttribute(string firstName, string lastName, int replyDepth = 1) + : base(new RepliedUserFilter(firstName, lastName, StringComparison.InvariantCulture, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message is from a user with a specific first name. + /// + /// The first name to match + /// How many levels up the reply chain to check (default: 1) + public RepliedFromUserAttribute(string firstName, int replyDepth = 1) + : base(new RepliedUserFilter(firstName, null, StringComparison.InvariantCulture, replyDepth)) { } + + /// + /// Initializes the attribute to filter messages where the replied-to message is from a user with a specific first name and custom comparison. + /// + /// The first name to match + /// The string comparison method + /// How many levels up the reply chain to check (default: 1) + public RepliedFromUserAttribute(string firstName, StringComparison comparison, int replyDepth = 1) + : base(new RepliedUserFilter(firstName, null, comparison, replyDepth)) { } + } + + /// + /// Attribute for filtering messages based on the user ID of the sender of the replied-to message. + /// + /// The user ID to match + /// How many levels up the reply chain to check (default: 1) + public class RepliedUserIdAttribute(long userId, int replyDepth = 1) + : MessageFilterAttribute(new RepliedUserIdFilter(userId, replyDepth)) + { } + + /// + /// Attribute for filtering messages where the replied-to message was sent by a bot. + /// + /// How many levels up the reply chain to check (default: 1) + public class ReplyFromBotAttribute(int replyDepth = 1) + : MessageFilterAttribute(new ReplyFromBotFilter(replyDepth)) + { } + + /// + /// Attribute for filtering messages where the replied-to message was sent by a premium user. + /// + /// How many levels up the reply chain to check (default: 1) + public class ReplyFromPremiumUserAttribute(int replyDepth = 1) + : MessageFilterAttribute(new ReplyFromPremiumUserFilter(replyDepth)) + { } +} diff --git a/Telegrator/Annotations/RepliedMessageTextFilterAttributes.cs b/Telegrator/Annotations/RepliedMessageTextFilterAttributes.cs new file mode 100644 index 0000000..1764e0e --- /dev/null +++ b/Telegrator/Annotations/RepliedMessageTextFilterAttributes.cs @@ -0,0 +1,52 @@ +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering updates where the replied-to message's text starts with the specified content. + /// + /// The string that the replied message's text should start with + /// The string comparison type + /// How many levels up the reply chain to check (default: 1) + public class RepliedTextStartsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1) + : MessageFilterAttribute(new RepliedTextStartsWithFilter(content, comparison, replyDepth)) + { } + + /// + /// Attribute for filtering updates where the replied-to message's text ends with the specified content. + /// + /// The string that the replied message's text should end with + /// The string comparison type + /// How many levels up the reply chain to check (default: 1) + public class RepliedTextEndsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1) + : MessageFilterAttribute(new RepliedTextEndsWithFilter(content, comparison, replyDepth)) + { } + + /// + /// Attribute for filtering updates where the replied-to message's text contains the specified content. + /// + /// The string that the replied message's text should contain + /// The string comparison type + /// How many levels up the reply chain to check (default: 1) + public class RepliedTextContainsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1) + : MessageFilterAttribute(new RepliedTextContainsFilter(content, comparison, replyDepth)) + { } + + /// + /// Attribute for filtering updates where the replied-to message's text equals the specified content. + /// + /// The string that the replied message's text should equal + /// The string comparison type + /// How many levels up the reply chain to check (default: 1) + public class RepliedTextEqualsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1) + : MessageFilterAttribute(new RepliedTextEqualsFilter(content, comparison, replyDepth)) + { } + + /// + /// Attribute for filtering updates where the replied-to message contains any non-empty text. + /// + /// How many levels up the reply chain to check (default: 1) + public class RepliedHasTextAttribute(int replyDepth = 1) + : MessageFilterAttribute(new RepliedTextNotNullOrEmptyFilter(replyDepth)) + { } +} diff --git a/Telegrator/Annotations/RepliedToMeAttribute.cs b/Telegrator/Annotations/RepliedToMeAttribute.cs new file mode 100644 index 0000000..e183383 --- /dev/null +++ b/Telegrator/Annotations/RepliedToMeAttribute.cs @@ -0,0 +1,11 @@ +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering messages with reply to messages of this bot. + /// + public class RepliedToMeAttribute() + : MessageFilterAttribute(new RepliedToMeFilter()) + { } +} diff --git a/Telegrator/Annotations/StateKeeping/EnumStateAttribute.cs b/Telegrator/Annotations/StateKeeping/EnumStateAttribute.cs new file mode 100644 index 0000000..9d84e7b --- /dev/null +++ b/Telegrator/Annotations/StateKeeping/EnumStateAttribute.cs @@ -0,0 +1,44 @@ +using Telegrator.StateKeeping; +using Telegrator.Attributes; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Annotations.StateKeeping +{ + /// + /// Attribute for managing enum-based states in Telegram bot handlers. + /// Provides a convenient way to associate enum values with state management functionality. + /// + /// The enum type to be used for state management. + public class EnumStateAttribute : StateKeeperAttribute> where TEnum : Enum + { + /// + /// Initializes a new instance of the EnumStateAttribute with a special state and custom key resolver. + /// + /// The special state to be managed. + /// The resolver for extracting keys from updates. + public EnumStateAttribute(SpecialState specialState, IStateKeyResolver keyResolver) + : base(specialState, keyResolver) { } + + /// + /// Initializes a new instance of the EnumStateAttribute with a specific enum state and custom key resolver. + /// + /// The specific enum state to be managed. + /// The resolver for extracting keys from updates. + public EnumStateAttribute(TEnum myState, IStateKeyResolver keyResolver) + : base(myState, keyResolver) { } + + /// + /// Initializes a new instance of the EnumStateAttribute with a special state and default sender ID resolver. + /// + /// The special state to be managed. + public EnumStateAttribute(SpecialState specialState) + : base(specialState, new SenderIdResolver()) { } + + /// + /// Initializes a new instance of the EnumStateAttribute with a specific enum state and default sender ID resolver. + /// + /// The specific enum state to be managed. + public EnumStateAttribute(TEnum myState) + : this(myState, new SenderIdResolver()) { } + } +} diff --git a/Telegrator/Annotations/StateKeeping/NumericStateAttribute.cs b/Telegrator/Annotations/StateKeeping/NumericStateAttribute.cs new file mode 100644 index 0000000..43efd83 --- /dev/null +++ b/Telegrator/Annotations/StateKeeping/NumericStateAttribute.cs @@ -0,0 +1,43 @@ +using Telegrator.StateKeeping; +using Telegrator.Attributes; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Annotations.StateKeeping +{ + /// + /// Attribute for associating a handler or method with a numeric (integer) state keeper. + /// Provides constructors for flexible state and key resolver configuration. + /// + public class NumericStateAttribute : StateKeeperAttribute + { + /// + /// Initializes the attribute with a special state and a custom key resolver. + /// + /// The special state to associate + /// The key resolver for state keeping + public NumericStateAttribute(SpecialState specialState, IStateKeyResolver keyResolver) + : base(specialState, keyResolver) { } + + /// + /// Initializes the attribute with a specific numeric state and a custom key resolver. + /// + /// The integer state to associate + /// The key resolver for state keeping + public NumericStateAttribute(int myState, IStateKeyResolver keyResolver) + : base(myState, keyResolver) { } + + /// + /// Initializes the attribute with a special state and the default sender ID resolver. + /// + /// The special state to associate + public NumericStateAttribute(SpecialState specialState) + : base(specialState, new SenderIdResolver()) { } + + /// + /// Initializes the attribute with a specific numeric state and the default sender ID resolver. + /// + /// The integer state to associate + public NumericStateAttribute(int myState) + : this(myState, new SenderIdResolver()) { } + } +} diff --git a/Telegrator/Annotations/StateKeeping/SpecialState.cs b/Telegrator/Annotations/StateKeeping/SpecialState.cs new file mode 100644 index 0000000..b599448 --- /dev/null +++ b/Telegrator/Annotations/StateKeeping/SpecialState.cs @@ -0,0 +1,21 @@ +namespace Telegrator.Annotations.StateKeeping +{ + /// + /// Represents special states for state keeping logic. + /// + public enum SpecialState + { + /// + /// No special state. + /// + None, + /// + /// Indicates that no state is present. + /// + NoState, + /// + /// Indicates that any state is acceptable. + /// + AnyState + } +} diff --git a/Telegrator/Annotations/StateKeeping/StringStateAttribute.cs b/Telegrator/Annotations/StateKeeping/StringStateAttribute.cs new file mode 100644 index 0000000..e2f684a --- /dev/null +++ b/Telegrator/Annotations/StateKeeping/StringStateAttribute.cs @@ -0,0 +1,77 @@ +using Telegrator.StateKeeping; +using Telegrator.Attributes; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Annotations.StateKeeping +{ + /// + /// Attribute for associating a handler or method with a string-based state keeper. + /// Provides various constructors for flexible state and key resolver configuration. + /// + public class StringStateAttribute : StateKeeperAttribute + { + /// + /// Initializes the attribute with a special state and a custom key resolver. + /// + /// The special state to associate + /// The key resolver for state keeping + public StringStateAttribute(SpecialState specialState, IStateKeyResolver keyResolver) + : base(specialState, keyResolver) { } + + /// + /// Initializes the attribute with a specific state and a custom key resolver. + /// + /// The string state to associate + /// The key resolver for state keeping + public StringStateAttribute(string myState, IStateKeyResolver keyResolver) + : base(myState, keyResolver) { } + + /// + /// Initializes the attribute with a special state and the default sender ID resolver. + /// + /// The special state to associate + public StringStateAttribute(SpecialState specialState) + : base(specialState, new SenderIdResolver()) { } + + /// + /// Initializes the attribute with a specific state and the default sender ID resolver. + /// + /// The string state to associate + public StringStateAttribute(string myState) + : base(myState, new SenderIdResolver()) { } + + /// + /// Initializes the attribute with a specific state, a custom key resolver, and a set of possible states. + /// + /// The string state to associate + /// The key resolver for state keeping + /// The set of possible string states + public StringStateAttribute(string myState, IStateKeyResolver keyResolver, params string[] states) + : base(new StringStateKeeper(states), myState, keyResolver) { } + + /// + /// Initializes the attribute with a special state, a custom key resolver, and a set of possible states. + /// + /// The special state to associate + /// The key resolver for state keeping + /// The set of possible string states + public StringStateAttribute(SpecialState specialState, IStateKeyResolver keyResolver, params string[] states) + : base(new StringStateKeeper(states), specialState, keyResolver) { } + + /// + /// Initializes the attribute with a specific state, the default sender ID resolver, and a set of possible states. + /// + /// The string state to associate + /// The set of possible string states + public StringStateAttribute(string myState, params string[] states) + : base(new StringStateKeeper(states), myState, new SenderIdResolver()) { } + + /// + /// Initializes the attribute with a special state, the default sender ID resolver, and a set of possible states. + /// + /// The special state to associate + /// The set of possible string states + public StringStateAttribute(SpecialState specialState, params string[] states) + : base(new StringStateKeeper(states), specialState, new SenderIdResolver()) { } + } +} diff --git a/Telegrator/Annotations/WelcomeAttribute.cs b/Telegrator/Annotations/WelcomeAttribute.cs new file mode 100644 index 0000000..8118498 --- /dev/null +++ b/Telegrator/Annotations/WelcomeAttribute.cs @@ -0,0 +1,18 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; + +namespace Telegrator.Annotations +{ + /// + /// Attribute for filtering message with command "start" in bot's private chats. + /// Allows handlers to respond to "welcome" bot commands. + /// + public class WelcomeAttribute : MessageFilterAttribute + { + /// + /// Creates new instance of + /// + public WelcomeAttribute() : base(new MessageChatTypeFilter(ChatType.Private), new CommandAlliasFilter("start")) + { } + } +} diff --git a/Telegrator/Attributes/Components/StateKeeperAttributeBase.cs b/Telegrator/Attributes/Components/StateKeeperAttributeBase.cs new file mode 100644 index 0000000..f1b5262 --- /dev/null +++ b/Telegrator/Attributes/Components/StateKeeperAttributeBase.cs @@ -0,0 +1,35 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Attributes.Components +{ + /// + /// Sets the state in which the can be executed + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public abstract class StateKeeperAttributeBase : Attribute, IFilter + { + /// + public bool IsCollectible => this.HasPublicProperties(); + + /// + /// Creates a new instance + /// + /// + /// + protected StateKeeperAttributeBase(Type stateKeeperType) + { + if (!stateKeeperType.IsAssignableToGenericType(typeof(StateKeeperBase<,>))) + throw new ArgumentException(stateKeeperType + " is not a StateKeeperBase", nameof(stateKeeperType)); + } + + /// + /// Realizes a for validation of the current in the polling routing + /// + /// + /// + public abstract bool CanPass(FilterExecutionContext context); + } +} diff --git a/Telegrator/Attributes/Components/UpdateFilterAttributeBase.cs b/Telegrator/Attributes/Components/UpdateFilterAttributeBase.cs new file mode 100644 index 0000000..069a2ec --- /dev/null +++ b/Telegrator/Attributes/Components/UpdateFilterAttributeBase.cs @@ -0,0 +1,47 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Attributes; +using Telegrator.Filters; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; + +namespace Telegrator.Attributes.Components +{ + /// + /// Defines the to validation for entry into execution of the + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public abstract class UpdateFilterAttributeBase : Attribute + { + /// + /// Gets the 's that processing + /// + public abstract UpdateType[] AllowedTypes { get; } + + /// + /// Gets the that processing + /// + public abstract Filter AnonymousFilter { get; protected set; } + + /// + /// Gets or sets the filter modifiers that affect how this filter is combined with others. + /// + public FilterModifier Modifiers { get; set; } + + /// + /// Creates a new instance of + /// + /// + protected internal UpdateFilterAttributeBase() + { + if (AllowedTypes.Length == 0) + throw new ArgumentException(); + } + + /// + /// Determines the logic of filter modifiers. Exceptionally internal implementation + /// + /// + public abstract bool ProcessModifiers(UpdateFilterAttributeBase? previous); + } +} diff --git a/Telegrator/Attributes/Components/UpdateHandlerAttributeBase.cs b/Telegrator/Attributes/Components/UpdateHandlerAttributeBase.cs new file mode 100644 index 0000000..fb1ceb1 --- /dev/null +++ b/Telegrator/Attributes/Components/UpdateHandlerAttributeBase.cs @@ -0,0 +1,77 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Attributes.Components +{ + /// + /// Defines the 's and validator () of the that will process + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public abstract class UpdateHandlerAttributeBase : Attribute, IFilter + { + /// + public bool IsCollectible => this.HasPublicProperties(); + + /// + /// Gets an array of that this attribute can be attached to + /// + public Type[] ExpectingHandlerType { get; private set; } + + /// + /// Gets an that handlers processes + /// + public UpdateType Type { get; private set; } + + /// + /// Gets or sets concurrency of this in same pool + /// + public int Concurrency { get; set; } + + /// + /// Gets or sets priority of this in same type handlers pool + /// + public int Priority { get; set; } + + /// + /// Creates a new instance of + /// + /// + /// + /// + /// + /// + /// + protected internal UpdateHandlerAttributeBase(Type[] expectingHandlerType, UpdateType updateType, int concurrency = 0) + { + if (expectingHandlerType == null) + throw new ArgumentNullException(nameof(expectingHandlerType)); + + if (expectingHandlerType.Any(type => !type.IsHandlerAbstract())) + throw new ArgumentException("One of expectingHandlerType is not a handler type", nameof(expectingHandlerType)); + + if (updateType == UpdateType.Unknown) + throw new Exception(); + + ExpectingHandlerType = expectingHandlerType; + Type = updateType; + Concurrency = concurrency; + } + + /// + /// Gets an of this from and + /// + /// + public DescriptorIndexer GetIndexer() + => new DescriptorIndexer(0, this); + + /// + /// Validator () of the that will process + /// + /// + /// + public abstract bool CanPass(FilterExecutionContext context); + } +} diff --git a/Telegrator/Attributes/DontCollectAttribute.cs b/Telegrator/Attributes/DontCollectAttribute.cs new file mode 100644 index 0000000..c9766b7 --- /dev/null +++ b/Telegrator/Attributes/DontCollectAttribute.cs @@ -0,0 +1,12 @@ +namespace Telegrator.Attributes +{ + /// + /// Attribute that prevents a class from being automatically collected by the handler collection system. + /// When applied to a class, it will be excluded from domain-wide handler collection operations. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class DontCollectAttribute : Attribute + { + + } +} diff --git a/Telegrator/Attributes/FilterModifier.cs b/Telegrator/Attributes/FilterModifier.cs new file mode 100644 index 0000000..b25c89d --- /dev/null +++ b/Telegrator/Attributes/FilterModifier.cs @@ -0,0 +1,25 @@ +namespace Telegrator.Attributes +{ + /// + /// Enumeration of filter modifiers that can be applied to update filters. + /// Defines how filters should be combined and applied in filter chains. + /// + [Flags] + public enum FilterModifier + { + /// + /// No modifier applied. Filter is applied as-is. + /// + None = 1, + + /// + /// OR modifier. This filter or the next filter in the chain should match. + /// + OrNext = 2, + + /// + /// NOT modifier. The inverse of this filter should match. + /// + Not = 4, + } +} diff --git a/Telegrator/Attributes/MightAwaitAttribute.cs b/Telegrator/Attributes/MightAwaitAttribute.cs new file mode 100644 index 0000000..3a3fbc1 --- /dev/null +++ b/Telegrator/Attributes/MightAwaitAttribute.cs @@ -0,0 +1,25 @@ +using Telegram.Bot.Types.Enums; + +namespace Telegrator.Attributes +{ + /// + /// Attribute that says if this handler cn await some of await types, that is not listed by its handler base + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class MightAwaitAttribute : Attribute + { + private readonly UpdateType[] _updateTypes; + + /// + /// Update types that may be awaited + /// + public UpdateType[] UpdateTypes => _updateTypes; + + /// + /// main ctor of + /// + /// + public MightAwaitAttribute(params UpdateType[] updateTypes) + => _updateTypes = updateTypes; + } +} diff --git a/Telegrator/Attributes/StateKeeperAttribute.cs b/Telegrator/Attributes/StateKeeperAttribute.cs new file mode 100644 index 0000000..158cc78 --- /dev/null +++ b/Telegrator/Attributes/StateKeeperAttribute.cs @@ -0,0 +1,106 @@ +using Telegram.Bot.Types; +using Telegrator.Annotations.StateKeeping; +using Telegrator.Attributes.Components; +using Telegrator.Filters.Components; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Attributes +{ + /// + /// Abstract attribute for associating a handler or method with a state keeper. + /// Provides logic for state-based filtering and state management. + /// + /// The type of the key used for state keeping (e.g., chat ID). + /// The type of the state value (e.g., string, int). + /// The type of the state keeper implementation. + public abstract class StateKeeperAttribute : StateKeeperAttributeBase where TKey : notnull where TState : notnull where TKeeper : StateKeeperBase, new() + { + /// + /// Gets or sets the singleton instance of the state keeper for this attribute type. + /// + public static TKeeper StateKeeper { get; internal set; } = null!; + + /// + /// Gets the state value associated with this attribute instance. + /// + public TState MyState { get; private set; } + + /// + /// Gets the special state mode for this attribute instance. + /// + public SpecialState SpecialState { get; private set; } + + /// + /// Initializes the attribute with a specific state and a custom key resolver. + /// + /// The state value to associate + /// The key resolver for state keeping + protected StateKeeperAttribute(TState myState, IStateKeyResolver keyResolver) : base(typeof(TKeeper)) + { + StateKeeper ??= new TKeeper(); + StateKeeper.KeyResolver = keyResolver; + MyState = myState; + SpecialState = SpecialState.None; + } + + /// + /// Initializes the attribute with a special state and a custom key resolver. + /// + /// The special state mode + /// The key resolver for state keeping + protected StateKeeperAttribute(SpecialState specialState, IStateKeyResolver keyResolver) : base(typeof(TKeeper)) + { + StateKeeper ??= new TKeeper(); + StateKeeper.KeyResolver = keyResolver; + MyState = StateKeeper.DefaultState; + SpecialState = specialState; + } + + /// + /// Initializes the attribute with a custom state keeper, a specific state, and a custom key resolver. + /// + /// The state keeper instance + /// The state value to associate + /// The key resolver for state keeping + protected StateKeeperAttribute(TKeeper keeper, TState myState, IStateKeyResolver keyResolver) : base(typeof(TKeeper)) + { + StateKeeper ??= keeper; + StateKeeper.KeyResolver = keyResolver; + MyState = myState; + SpecialState = SpecialState.None; + } + + /// + /// Initializes the attribute with a custom state keeper, a special state, and a custom key resolver. + /// + /// The state keeper instance + /// The special state mode + /// The key resolver for state keeping + protected StateKeeperAttribute(TKeeper keeper, SpecialState specialState, IStateKeyResolver keyResolver) : base(typeof(TKeeper)) + { + StateKeeper ??= keeper; + StateKeeper.KeyResolver = keyResolver; + MyState = StateKeeper.DefaultState; + SpecialState = specialState; + } + + /// + /// Determines whether the current update context passes the state filter. + /// + /// The filter execution context + /// True if the state matches the filter; otherwise, false. + public override bool CanPass(FilterExecutionContext 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); + } + } +} diff --git a/Telegrator/Attributes/UpdateFilterAttribute.cs b/Telegrator/Attributes/UpdateFilterAttribute.cs new file mode 100644 index 0000000..d578440 --- /dev/null +++ b/Telegrator/Attributes/UpdateFilterAttribute.cs @@ -0,0 +1,73 @@ +using Telegram.Bot.Types; +using Telegrator.Attributes.Components; +using Telegrator.Filters; +using Telegrator.Filters.Components; + +namespace Telegrator.Attributes +{ + /// + /// Abstract base attribute for defining update filters for a specific type of update target. + /// Provides logic for filter composition, modifier processing, and target extraction. + /// + /// The type of the update target to filter (e.g., Message, Update). + public abstract class UpdateFilterAttribute : UpdateFilterAttributeBase where T : class + { + /// + /// Gets the compiled anonymous filter for this attribute. + /// + public override Filter AnonymousFilter { get; protected set; } + + /// + /// Gets the compiled filter logic for the update target. + /// + public Filter UpdateFilter { get; private set; } + + /// + /// Initializes the attribute with one or more filters for the update target. + /// + /// The filters to compose + protected UpdateFilterAttribute(params IFilter[] filters) + { + UpdateFilter = CompiledFilter.Compile(filters); + AnonymousFilter = AnonymousTypeFilter.Compile(UpdateFilter, GetFilterringTarget); + } + + /// + /// Initializes the attribute with a precompiled filter for the update target. + /// + /// The compiled filter + protected UpdateFilterAttribute(Filter updateFilter) + { + UpdateFilter = updateFilter; + AnonymousFilter = AnonymousTypeFilter.Compile(UpdateFilter, GetFilterringTarget); + } + + /// + /// Processes filter modifiers and combines this filter with the previous one if needed. + /// + /// The previous filter attribute in the chain + /// True if the OrNext modifier is set; otherwise, false. + public override sealed bool ProcessModifiers(UpdateFilterAttributeBase? previous) + { + if (Modifiers.HasFlag(FilterModifier.Not)) + AnonymousFilter = AnonymousFilter.Not(); + + if (previous is not null) + { + if (previous.Modifiers.HasFlag(FilterModifier.OrNext)) + { + AnonymousFilter = previous.AnonymousFilter.Or(AnonymousFilter); + } + } + + return Modifiers.HasFlag(FilterModifier.OrNext); + } + + /// + /// Extracts the filtering target of type from the given update. + /// + /// The Telegram update + /// The target object to filter, or null if not applicable + public abstract T? GetFilterringTarget(Update update); + } +} diff --git a/Telegrator/Attributes/UpdateHandlerAttribute.cs b/Telegrator/Attributes/UpdateHandlerAttribute.cs new file mode 100644 index 0000000..1fa748a --- /dev/null +++ b/Telegrator/Attributes/UpdateHandlerAttribute.cs @@ -0,0 +1,18 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Attributes.Components; +using Telegrator.Handlers.Components; + +namespace Telegrator.Attributes +{ + /// + /// Abstract base attribute for marking update handler classes. + /// Provides a type-safe way to associate handler types with specific update types and concurrency settings. + /// + /// The type of the update handler that this attribute is applied to. + /// The type of update that this handler can process. + /// The concurrency level for this handler (default: 0 for unlimited). + public abstract class UpdateHandlerAttribute(UpdateType updateType, int concurrency = 0) + : UpdateHandlerAttributeBase([typeof(T)], updateType, concurrency) where T : UpdateHandlerBase + { + } +} diff --git a/Telegrator/Configuration/IHandlersCollectingOptions.cs b/Telegrator/Configuration/IHandlersCollectingOptions.cs new file mode 100644 index 0000000..912aba5 --- /dev/null +++ b/Telegrator/Configuration/IHandlersCollectingOptions.cs @@ -0,0 +1,19 @@ +namespace Telegrator.Configuration +{ + /// + /// Interface for configuring handler collection behavior. + /// Defines options that control how handlers are collected and processed during initialization. + /// + public interface IHandlersCollectingOptions + { + /// + /// Gets or sets a value indicating whether to descend the indexr of handler's index on register. ('false' by default) + /// + public bool DescendDescriptorIndex { get; set; } + + /// + /// Gets or sets a value indicating whether to exclude intersecting command aliases. + /// + public bool ExceptIntersectingCommandAliases { get; set; } + } +} diff --git a/Telegrator/Configuration/ITelegramBotInfo.cs b/Telegrator/Configuration/ITelegramBotInfo.cs new file mode 100644 index 0000000..9c1aa2c --- /dev/null +++ b/Telegrator/Configuration/ITelegramBotInfo.cs @@ -0,0 +1,16 @@ +using Telegram.Bot.Types; + +namespace Telegrator.Configuration +{ + /// + /// Interface for providing bot information and metadata. + /// Contains information about the bot user and provides initialization capabilities. + /// + public interface ITelegramBotInfo + { + /// + /// Gets the representing the bot. + /// + public User User { get; } + } +} diff --git a/Telegrator/Configuration/TelegramBotOptions.cs b/Telegrator/Configuration/TelegramBotOptions.cs new file mode 100644 index 0000000..57f90ef --- /dev/null +++ b/Telegrator/Configuration/TelegramBotOptions.cs @@ -0,0 +1,29 @@ +namespace Telegrator.Configuration +{ + /// + /// Configuration options for Telegram bot behavior and execution settings. + /// Controls various aspects of bot operation including concurrency, routing, and execution policies. + /// + public class TelegramBotOptions + { + /// + /// Gets or sets a value indicating whether only the first found handler should be executed for each update. + /// + public bool ExecuteOnlyFirstFoundHanlder { get; set; } + + /// + /// Gets or sets the maximum number of parallel working handlers. Null means no limit. + /// + public int? MaximumParallelWorkingHandlers { get; set; } + + /// + /// Gets or sets a value indicating whether awaiting handlers should be routed separately from regular handlers. + /// + public bool ExclusiveAwaitingHandlerRouting { get; set; } + + /// + /// Gets or sets the global cancellation token for all bot operations. + /// + public CancellationToken GlobalCancellationToken { get; set; } + } +} diff --git a/Telegrator/Enums.cs b/Telegrator/Enums.cs new file mode 100644 index 0000000..8fc0a98 --- /dev/null +++ b/Telegrator/Enums.cs @@ -0,0 +1,39 @@ +namespace Telegrator +{ + /// + /// Enumeration of dice types supported by Telegram. + /// Used for filtering dice messages and determining dice emoji representations. + /// + public enum DiceType + { + /// + /// Standard dice (🎲). + /// + Dice, + + /// + /// Darts (🎯). + /// + Darts, + + /// + /// Bowling (🎳). + /// + Bowling, + + /// + /// Basketball (🏀). + /// + Basketball, + + /// + /// Football (⚽). + /// + Football, + + /// + /// Casino slot machine (🎰). + /// + Casino + } +} diff --git a/Telegrator/Exceptions.cs b/Telegrator/Exceptions.cs new file mode 100644 index 0000000..28cbac6 --- /dev/null +++ b/Telegrator/Exceptions.cs @@ -0,0 +1,52 @@ +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator +{ + /// + /// Exception thrown when attempting to modify a frozen collection. + /// + public class CollectionFrozenException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public CollectionFrozenException() + : base("Can't change a frozen collection.") { } + } + + /// + /// Exception thrown when a type is not a valid filter type. + /// + public class NotFilterTypeException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The type that is not a filter type. + public NotFilterTypeException(Type type) + : base(string.Format("\"{0}\" is not a filter type", type.Name)) { } + } + + /// + /// Exception thrown when a handler execution fails. + /// Contains information about the handler and the inner exception. + /// + public class HandlerFaultedException : Exception + { + /// + /// The handler info associated with the faulted handler. + /// + public readonly DescribedHandlerInfo HandlerInfo; + + /// + /// Initializes a new instance of the class. + /// + /// The handler info. + /// The inner exception. + public HandlerFaultedException(DescribedHandlerInfo handlerInfo, Exception inner) + : base(string.Format("Handler's \"{0}\" execution was faulted", handlerInfo.DisplayString), inner) + { + HandlerInfo = handlerInfo; + } + } +} diff --git a/Telegrator/Filters/CommandAlliasFilter.cs b/Telegrator/Filters/CommandAlliasFilter.cs new file mode 100644 index 0000000..a84c50f --- /dev/null +++ b/Telegrator/Filters/CommandAlliasFilter.cs @@ -0,0 +1,32 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; +using Telegrator.Handlers; + +namespace Telegrator.Filters +{ + /// + /// Filter that checks if a command matches any of the specified aliases. + /// Requires a to be applied first to extract the command. + /// + /// The command aliases to check against. + public class CommandAlliasFilter(params string[] alliases) : Filter + { + /// + /// Gets the command that was received and extracted by the . + /// + public string ReceivedCommand { get; private set; } = string.Empty; + + /// + /// Checks if the received command matches any of the specified aliases. + /// This filter requires a to be applied first + /// to extract the command from the message. + /// + /// The filter execution context containing the completed filters. + /// True if the command matches any of the specified aliases; otherwise, false. + public override bool CanPass(FilterExecutionContext context) + { + ReceivedCommand = context.CompletedFilters.Get(0).ReceivedCommand; + return alliases.Contains(ReceivedCommand, StringComparer.InvariantCultureIgnoreCase); + } + } +} diff --git a/Telegrator/Filters/Components/AnonymousCompiledFilter.cs b/Telegrator/Filters/Components/AnonymousCompiledFilter.cs new file mode 100644 index 0000000..8267613 --- /dev/null +++ b/Telegrator/Filters/Components/AnonymousCompiledFilter.cs @@ -0,0 +1,56 @@ +using Telegram.Bot; +using Telegram.Bot.Types; + +namespace Telegrator.Filters.Components +{ + /// + /// Represents a compiled filter that applies a set of filters to an anonymous target type. + /// + public sealed class AnonymousCompiledFilter : AnonymousTypeFilter + { + /// + /// Initializes a new instance of the class. + /// + /// The filter action delegate. + /// The function to get the filtering target from an update. + private AnonymousCompiledFilter(Func, object, bool> filterAction, Func getFilterringTarget) + : base(filterAction, getFilterringTarget) { } + + /// + /// Compiles a set of filters into an for a specific target type. + /// + /// The type of the filtering target. + /// The list of filters to compile. + /// The function to get the filtering target from an update. + /// The compiled filter. + public static AnonymousCompiledFilter Compile(IList> filters, Func getFilterringTarget) where T : class + { + return new AnonymousCompiledFilter( + (context, filterringTarget) => CanPassInternal(filters, context, filterringTarget), + getFilterringTarget); + } + + /// + /// Determines whether all filters can pass for the given context and filtering target. + /// + /// The type of the filtering target. + /// The list of filters. + /// The filter execution context. + /// The filtering target. + /// True if all filters pass; otherwise, false. + private static bool CanPassInternal(IList> filters, FilterExecutionContext updateContext, object filterringTarget) where T : class + { + FilterExecutionContext context = updateContext.CreateChild((T)filterringTarget); + + foreach (IFilter filter in filters) + { + if (!filter.CanPass(context)) + return false; + + context.CompletedFilters.Add(filter); + } + + return true; + } + } +} diff --git a/Telegrator/Filters/Components/AnonymousTypeFilter.cs b/Telegrator/Filters/Components/AnonymousTypeFilter.cs new file mode 100644 index 0000000..3ae4993 --- /dev/null +++ b/Telegrator/Filters/Components/AnonymousTypeFilter.cs @@ -0,0 +1,77 @@ +using Telegram.Bot.Types; + +namespace Telegrator.Filters.Components +{ + /// + /// Represents a filter that applies a filter action to an anonymous target type extracted from an update. + /// + public class AnonymousTypeFilter : Filter + { + private readonly Func, object, bool> FilterAction; + private readonly Func GetFilterringTarget; + + /// + /// Initializes a new instance of the class. + /// + /// The filter action delegate. + /// The function to get the filtering target from an update. + protected AnonymousTypeFilter(Func, object, bool> filterAction, Func getFilterringTarget) + { + FilterAction = filterAction; + GetFilterringTarget = getFilterringTarget; + } + + /// + /// Compiles a filter for a specific target type. + /// + /// The type of the filtering target. + /// The filter to apply. + /// The function to get the filtering target from an update. + /// The compiled filter. + public static AnonymousTypeFilter Compile(IFilter filter, Func getFilterringTarget) where T : class + { + return new AnonymousTypeFilter( + (context, filterringTarget) => CanPassInternal(context, filter, filterringTarget), + getFilterringTarget); + } + + /// + /// Determines whether the filter can pass for the given context and filtering target. + /// + /// The type of the filtering target. + /// The filter execution context. + /// The filter to apply. + /// The filtering target. + /// True if the filter passes; otherwise, false. + private static bool CanPassInternal(FilterExecutionContext updateContext, IFilter filter, object filterringTarget) where T : class + { + FilterExecutionContext context = updateContext.CreateChild((T)filterringTarget); + if (!filter.CanPass(context)) + return false; + + context.CompletedFilters.Add(filter); + return true; + } + + /// + /// Determines whether the filter can pass for the given context by extracting the filtering target and applying the filter action. + /// + /// The filter execution context. + /// True if the filter passes; otherwise, false. + public override bool CanPass(FilterExecutionContext context) + { + try + { + object? filterringTarget = GetFilterringTarget.Invoke(context.Input); + if (filterringTarget == null) + return false; + + return FilterAction.Invoke(context, filterringTarget); + } + catch + { + return false; + } + } + } +} diff --git a/Telegrator/Filters/Components/CompiledFilter.cs b/Telegrator/Filters/Components/CompiledFilter.cs new file mode 100644 index 0000000..bdb4fc8 --- /dev/null +++ b/Telegrator/Filters/Components/CompiledFilter.cs @@ -0,0 +1,48 @@ +namespace Telegrator.Filters.Components +{ + /// + /// Represents a filter that composes multiple filters and passes only if all of them pass. + /// + /// The type of the input for the filter. + public class CompiledFilter : Filter where T : class + { + private readonly IFilter[] Filters; + + /// + /// Initializes a new instance of the class. + /// + /// The filters to compose. + private CompiledFilter(IFilter[] filters) + { + Filters = filters; + } + + /// + /// Compiles multiple filters into a . + /// + /// The filters to compose. + /// A new instance. + public static CompiledFilter Compile(params IFilter[] filters) + { + return new CompiledFilter(filters); + } + + /// + /// Determines whether all composed filters pass for the given context. + /// + /// The filter execution context. + /// True if all filters pass; otherwise, false. + public override bool CanPass(FilterExecutionContext context) + { + foreach (IFilter filter in Filters) + { + if (!filter.CanPass(context)) + return false; + + context.CompletedFilters.Add(filter); + } + + return true; + } + } +} diff --git a/Telegrator/Filters/Components/CompletedFiltersList.cs b/Telegrator/Filters/Components/CompletedFiltersList.cs new file mode 100644 index 0000000..343188f --- /dev/null +++ b/Telegrator/Filters/Components/CompletedFiltersList.cs @@ -0,0 +1,86 @@ +using System.Collections; + +namespace Telegrator.Filters.Components +{ + /// + /// The list containing filters worked out during Polling to further obtain additional filtering information + /// + public class CompletedFiltersList : IEnumerable + { + private readonly List CompletedFilters = []; + + /// + /// Adds the completed filter to the list. + /// + /// The type of update. + /// The filter to add. + public void Add(IFilter filter) where TUpdate : class + { + if (filter is AnonymousTypeFilter | filter is AnonymousCompiledFilter) + return; + + if (!filter.IsCollectible) + return; + + CompletedFilters.Add(filter); + } + + /// + /// Adds many completed filters to the list. + /// + /// The type of update. + /// The filters to add. + public void AddRange(IEnumerable> filters) where TUpdate : class + { + foreach (IFilter filter in filters) + Add(filter); + } + + /// + /// Looks for filters of a given type in the list. + /// + /// The filter type to search for. + /// The enumerable containing filters of the given type. + /// Thrown if the type is not a filter type. + public IEnumerable Get() where TFilter : notnull, IFilterCollectable + { + if (!typeof(TFilter).IsFilterType()) + throw new NotFilterTypeException(typeof(TFilter)); + + return CompletedFilters.WhereCast(); + } + + /// + /// Looks for a filter of a given type at the specified index in the list. + /// + /// The filter type to search for. + /// The index of the filter. + /// The filter of the given type at the specified index. + /// Thrown if the type is not a filter type. + /// Thrown if no filter is found at the index. + public TFilter Get(int index) where TFilter : notnull, IFilterCollectable + { + IEnumerable filters = Get(); + return filters.Any() ? filters.ElementAt(index) : throw new KeyNotFoundException(); + } + + /// + /// Returns a filter of a given type at the specified index, or null if it does not exist. + /// + /// The filter type to search for. + /// The index of the filter. + /// The filter at the specified index, or null if it does not exist. + /// Thrown if the type is not a filter type. + public TFilter? GetOrDefault(int index) where TFilter : IFilterCollectable + { + IEnumerable filters = Get(); + return filters.Any() ? filters.ElementAt(index) : default; + } + + /// + public IEnumerator GetEnumerator() => CompletedFilters.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => CompletedFilters.GetEnumerator(); + } +} diff --git a/Telegrator/Filters/Components/FilterExecutionContext.cs b/Telegrator/Filters/Components/FilterExecutionContext.cs new file mode 100644 index 0000000..23c2fb7 --- /dev/null +++ b/Telegrator/Filters/Components/FilterExecutionContext.cs @@ -0,0 +1,79 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Configuration; + +namespace Telegrator.Filters.Components +{ + /// + /// Represents the context for filter execution, including update, input, and additional data. + /// + /// The type of the input for the filter. + public class FilterExecutionContext where T : class + { + /// + /// Gets the for the current context. + /// + public ITelegramBotInfo BotInfo { get; } + + /// + /// Gets the additional data dictionary for the context. + /// + public Dictionary Data { get; } + + /// + /// Gets the list of completed filters for the context. + /// + public CompletedFiltersList CompletedFilters { get; } + + /// + /// Gets the being processed. + /// + public Update Update { get; } + + /// + /// Gets the of the update. + /// + public UpdateType Type { get; } + + /// + /// Gets the input object for the filter. + /// + public T Input { get; } + + /// + /// Initializes a new instance of the class with all parameters. + /// + /// The bot info. + /// The update. + /// The input object. + /// The additional data dictionary. + /// The list of completed filters. + public FilterExecutionContext(ITelegramBotInfo botInfo, Update update, T input, Dictionary data, CompletedFiltersList completedFilters) + { + BotInfo = botInfo; + Data = data; + CompletedFilters = completedFilters; + Update = update; + Type = update.Type; + Input = input; + } + + /// + /// Initializes a new instance of the class with default data and filters. + /// + /// The bot info. + /// The update. + /// The input object. + public FilterExecutionContext(ITelegramBotInfo botInfo, Update update, T input) + : this(botInfo, update, input, [], []) { } + + /// + /// Creates a child context for a different input type, sharing the same data and completed filters. + /// + /// The type of the new input. + /// The new input object. + /// A new instance. + public FilterExecutionContext CreateChild(C input) where C : class + => new FilterExecutionContext(BotInfo, Update, input, Data, CompletedFilters); + } +} diff --git a/Telegrator/Filters/Components/IFilter.cs b/Telegrator/Filters/Components/IFilter.cs new file mode 100644 index 0000000..fc4c86a --- /dev/null +++ b/Telegrator/Filters/Components/IFilter.cs @@ -0,0 +1,28 @@ +namespace Telegrator.Filters.Components +{ + /// + /// Interface for filters that can be collected into a completed filters list. + /// Provides information about whether a filter should be tracked during execution. + /// + public interface IFilterCollectable + { + /// + /// Gets if filter can be collected to + /// + public bool IsCollectible { get; } + } + + /// + /// Represents a filter for a specific update type. + /// + /// The type of the update to filter. + public interface IFilter : IFilterCollectable where T : class + { + /// + /// Determines whether the filter can pass for the given context. + /// + /// The filter execution context. + /// True if the filter passes; otherwise, false. + public bool CanPass(FilterExecutionContext info); + } +} diff --git a/Telegrator/Filters/Components/IJoinedFilter.cs b/Telegrator/Filters/Components/IJoinedFilter.cs new file mode 100644 index 0000000..371d07c --- /dev/null +++ b/Telegrator/Filters/Components/IJoinedFilter.cs @@ -0,0 +1,14 @@ +namespace Telegrator.Filters.Components +{ + /// + /// Represents a filter that joins multiple filters together. + /// + /// The type of the input for the filter. + public interface IJoinedFilter : IFilter where T : class + { + /// + /// Gets the array of joined filters. + /// + public IFilter[] Filters { get; } + } +} diff --git a/Telegrator/Filters/EnvironmentFilters.cs b/Telegrator/Filters/EnvironmentFilters.cs new file mode 100644 index 0000000..65c92bb --- /dev/null +++ b/Telegrator/Filters/EnvironmentFilters.cs @@ -0,0 +1,125 @@ +using System.Diagnostics; +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Abstract base class for filters that operate based on the current environment. + /// Provides functionality to detect debug vs release environments. + /// + public abstract class EnvironmentFilter : Filter + { + /// + /// Gets a value indicating whether the current environment is debug mode. + /// This is set during static initialization based on the DEBUG conditional compilation symbol. + /// + protected static bool IsCurrentEnvDebug { get; private set; } = false; + + /// + /// Static constructor that initializes the environment detection. + /// + static EnvironmentFilter() + => SetIsCurrentEnvDebug(); + + /// + /// Sets the debug environment flag. This method is only compiled in DEBUG builds. + /// + [Conditional("DEBUG")] + private static void SetIsCurrentEnvDebug() + => IsCurrentEnvDebug = true; + } + + /// + /// Filter that only passes in debug environment builds. + /// + public class IsDebugEnvironmentFilter() : EnvironmentFilter + { + /// + /// Checks if the current environment is debug mode. + /// + /// The filter execution context (unused). + /// True if the current environment is debug mode; otherwise, false. + public override bool CanPass(FilterExecutionContext _) + => IsCurrentEnvDebug; + } + + /// + /// Filter that only passes in release environment builds. + /// + public class IsReleaseEnvironmentFilter() : EnvironmentFilter + { + /// + /// Checks if the current environment is release mode. + /// + /// The filter execution context (unused). + /// True if the current environment is release mode; otherwise, false. + public override bool CanPass(FilterExecutionContext _) + => !IsCurrentEnvDebug; + } + + /// + /// Filter that checks environment variable values. + /// + /// The environment variable name to check. + /// The expected value of the environment variable (optional). + /// The string comparison type to use for value matching. + public class EnvironmentVariableFilter(string variable, string? value, StringComparison comparison) : Filter + { + /// + /// The environment variable name to check. + /// + private readonly string _variable = variable; + + /// + /// The expected value of the environment variable (optional). + /// + private readonly string? _value = value; + + /// + /// The string comparison type to use for value matching. + /// + private readonly StringComparison _comparison = comparison; + + /// + /// Initializes a new instance of the class with a specific value. + /// + /// The environment variable name to check. + /// The expected value of the environment variable. + public EnvironmentVariableFilter(string variable, string? value) + : this(variable, value, StringComparison.InvariantCulture) { } + + /// + /// Initializes a new instance of the class that checks for non-null values. + /// + /// The environment variable name to check. + public EnvironmentVariableFilter(string variable) + : this(variable, "{NOT_NULL}", StringComparison.InvariantCulture) { } + + /// + /// Initializes a new instance of the class with custom comparison. + /// + /// The environment variable name to check. + /// The string comparison type to use. + public EnvironmentVariableFilter(string variable, StringComparison comparison) + : this(variable, "{NOT_NULL}", comparison) { } + + /// + /// Checks if the environment variable matches the expected criteria. + /// + /// The filter execution context (unused). + /// True if the environment variable matches the criteria; otherwise, false. + public override bool CanPass(FilterExecutionContext _) + { + string? envValue = Environment.GetEnvironmentVariable(_variable); + + if (envValue == null && _value == null) + return true; + + if (envValue == null) + return false; + + return envValue.Equals(_value, _comparison); + } + } +} diff --git a/Telegrator/Filters/Filter.cs b/Telegrator/Filters/Filter.cs new file mode 100644 index 0000000..f2fdd86 --- /dev/null +++ b/Telegrator/Filters/Filter.cs @@ -0,0 +1,113 @@ +using System.Reflection; +using Telegrator; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Base class for filters, providing logical operations and collectability. + /// + /// The type of the input for the filter. + public abstract class Filter : IFilter where T : class + { + /// + /// Creates a filter from a function. + /// + /// The filter function. + /// A instance. + public static Filter If(Func, bool> filter) + => new FunctionFilter(filter); + + /// + /// Creates a filter that always passes. + /// + /// An instance. + public static AnyFilter Any() + => new AnyFilter(); + + /// + /// Creates a filter that inverts the result of this filter. + /// + /// A instance. + public Filter Not() + => new ReverseFilter(this); + + /// + /// Creates a filter that passes only if both this and the specified filter pass. + /// + /// The filter to combine with. + /// An instance. + public AndFilter And(IFilter filter) + => new AndFilter(this, filter); + + /// + /// Creates a filter that passes if either this or the specified filter pass. + /// + /// The filter to combine with. + /// An instance. + public OrFilter Or(IFilter filter) + => new OrFilter(this, filter); + + /// + /// Gets a value indicating whether this filter is collectible. + /// + public bool IsCollectible => this.HasPublicProperties(); + + /// + /// Determines whether the filter can pass for the given context. + /// + /// The filter execution context. + /// True if the filter passes; otherwise, false. + public abstract bool CanPass(FilterExecutionContext context); + } + + /// + /// A filter that always passes. + /// + /// The type of the input for the filter. + public class AnyFilter : Filter where T : class + { + /// + public override bool CanPass(FilterExecutionContext context) + => true; + } + + /// + /// A filter that inverts the result of another filter. + /// + /// The type of the input for the filter. + public class ReverseFilter : Filter where T : class + { + private readonly IFilter filter; + + /// + /// Initializes a new instance of the class. + /// + /// The filter to invert. + public ReverseFilter(IFilter filter) + => this.filter = filter; + + /// + public override bool CanPass(FilterExecutionContext context) + => !filter.CanPass(context); + } + + /// + /// A filter that uses a function to determine if it passes. + /// + /// The type of the input for the filter. + public class FunctionFilter : Filter where T : class + { + private readonly Func, bool>? FilterFunc; + /// + /// Initializes a new instance of the class. + /// + /// The filter function. + public FunctionFilter(Func, bool> funcFilter) + => FilterFunc = funcFilter; + + /// + public override bool CanPass(FilterExecutionContext context) + => context.Input != null && FilterFunc != null && FilterFunc(context); + } +} diff --git a/Telegrator/Filters/JoinedFilter.cs b/Telegrator/Filters/JoinedFilter.cs new file mode 100644 index 0000000..b0bdba0 --- /dev/null +++ b/Telegrator/Filters/JoinedFilter.cs @@ -0,0 +1,54 @@ +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Base class for filters that join multiple filters together. + /// + /// The type of the input for the filter. + public abstract class JoinedFilter(params IFilter[] filters) : Filter, IJoinedFilter where T : class + { + /// + /// Gets the array of joined filters. + /// + public IFilter[] Filters { get; } = filters; + } + + /// + /// A filter that passes only if both joined filters pass. + /// + /// The type of the input for the filter. + public class AndFilter : JoinedFilter where T : class + { + /// + /// Initializes a new instance of the class. + /// + /// The left filter. + /// The right filter. + public AndFilter(IFilter leftFilter, IFilter rightFilter) + : base(leftFilter, rightFilter) { } + + /// + public override bool CanPass(FilterExecutionContext context) + => Filters[0].CanPass(context) && Filters[1].CanPass(context); + } + + /// + /// A filter that passes if at least one of the joined filters passes. + /// + /// The type of the input for the filter. + public class OrFilter : JoinedFilter where T : class + { + /// + /// Initializes a new instance of the class. + /// + /// The left filter. + /// The right filter. + public OrFilter(IFilter leftFilter, IFilter rightFilter) + : base(leftFilter, rightFilter) { } + + /// + public override bool CanPass(FilterExecutionContext context) + => Filters[0].CanPass(context) || Filters[1].CanPass(context); + } +} diff --git a/Telegrator/Filters/MentionedFilter.cs b/Telegrator/Filters/MentionedFilter.cs new file mode 100644 index 0000000..a486188 --- /dev/null +++ b/Telegrator/Filters/MentionedFilter.cs @@ -0,0 +1,52 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Filter that checks if a message contains a mention of the bot or a specific user. + /// Requires a to be applied first to identify mention entities. + /// + public class MentionedFilter : Filter + { + /// + /// The username to check for in the mention (null means check for bot's username). + /// + private readonly string? Mention; + + /// + /// Initializes a new instance of the class that checks for bot mentions. + /// + public MentionedFilter() + { + Mention = null; + } + + /// + /// Initializes a new instance of the class that checks for specific user mentions. + /// + /// The username to check for in the mention. + public MentionedFilter(string mention) + { + Mention = mention; + } + + /// + /// Checks if the message contains a mention of the specified user or bot. + /// This filter requires a to be applied first + /// to identify mention entities in the message. + /// + /// The filter execution context containing the message and completed filters. + /// True if the message contains the specified mention; otherwise, false. + /// Thrown when the bot username is null and no specific mention is provided. + public override bool CanPass(FilterExecutionContext context) + { + if (context.Input.Text == null) + return false; + + string userName = Mention ?? context.BotInfo.User.Username ?? throw new ArgumentNullException(nameof(context), "MentionedFilter requires BotInfo to be initialized"); + MessageHasEntityFilter entityFilter = context.CompletedFilters.Get(0); + return entityFilter.FoundEntities.Any(ent => context.Input.Text.Substring(ent.Offset + 1, ent.Length - 1) == userName); + } + } +} diff --git a/Telegrator/Filters/MessageChatFilters.cs b/Telegrator/Filters/MessageChatFilters.cs new file mode 100644 index 0000000..81b7c97 --- /dev/null +++ b/Telegrator/Filters/MessageChatFilters.cs @@ -0,0 +1,183 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Base class for filters that operate on the chat of the message being processed. + /// + public abstract class MessageChatFilter : Filter + { + /// + /// Gets the chat of the message being processed. + /// + public Chat Chat { get; private set; } = null!; + + /// + public override bool CanPass(FilterExecutionContext context) + { + Chat = context.Input.Chat; + return CanPassNext(context.CreateChild(Chat)); + } + + /// + /// Determines whether the filter passes for the given chat context. + /// + /// The filter execution context for the chat. + /// True if the filter passes; otherwise, false. + protected abstract bool CanPassNext(FilterExecutionContext _); + } + + /// + /// Filters messages whose chat is a forum. + /// + public class MessageChatIsForumFilter : MessageChatFilter + { + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Chat.IsForum; + } + + /// + /// Filters messages whose chat ID matches the specified value. + /// + public class MessageChatIdFilter(long id) : MessageChatFilter + { + private readonly long Id = id; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Chat.Id == Id; + } + + /// + /// Filters messages whose chat type matches the specified value. + /// + public class MessageChatTypeFilter(ChatType type) : MessageChatFilter + { + private readonly ChatType Type = type; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Chat.Type == Type; + } + + /// + /// Filters messages whose chat title matches the specified value. + /// + public class MessageChatTitleFilter : MessageChatFilter + { + private readonly string? Title; + private readonly StringComparison Comparison = StringComparison.InvariantCulture; + + /// + /// Initializes a new instance of the class. + /// + /// The chat title to match. + public MessageChatTitleFilter(string? title) => Title = title; + + /// + /// Initializes a new instance of the class with a specific string comparison. + /// + /// The chat title to match. + /// The string comparison to use. + public MessageChatTitleFilter(string? title, StringComparison comparison) + : this(title) => Comparison = comparison; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + { + if (Chat.Title == null) + return false; + + return Chat.Title.Equals(Title, Comparison); + } + } + + /// + /// Filters messages whose chat username matches the specified value. + /// + public class MessageChatUsernameFilter : MessageChatFilter + { + private readonly string? UserName; + private readonly StringComparison Comparison = StringComparison.InvariantCulture; + + /// + /// Initializes a new instance of the class. + /// + /// The chat username to match. + public MessageChatUsernameFilter(string? userName) => UserName = userName; + + /// + /// Initializes a new instance of the class with a specific string comparison. + /// + /// The chat username to match. + /// The string comparison to use. + public MessageChatUsernameFilter(string? userName, StringComparison comparison) + : this(userName) => Comparison = comparison; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + { + if (Chat.Username == null) + return false; + + return Chat.Username.Equals(UserName, Comparison); + } + } + + /// + /// Filters messages whose chat first and/or last name matches the specified values. + /// + public class MessageChatNameFilter : MessageChatFilter + { + private readonly string? FirstName; + private readonly string? LastName; + private readonly StringComparison Comparison = StringComparison.InvariantCulture; + + /// + /// Initializes a new instance of the class. + /// + /// The chat first name to match. + /// The chat last name to match. + public MessageChatNameFilter(string? firstName, string? lastName) + { + FirstName = firstName; + LastName = lastName; + } + + /// + /// Initializes a new instance of the class with a specific string comparison. + /// + /// The chat first name to match. + /// The chat last name to match. + /// The string comparison to use. + public MessageChatNameFilter(string? firstName, string? lastName, StringComparison comparison) + : this(firstName, lastName) => Comparison = comparison; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + { + if (LastName != null) + { + if (Chat.LastName == null) + return false; + + if (Chat.LastName.Equals(LastName, Comparison)) + return false; + } + + if (FirstName != null) + { + if (Chat.FirstName == null) + return false; + + if (Chat.FirstName.Equals(FirstName, Comparison)) + return false; + } + + return true; + } + } +} diff --git a/Telegrator/Filters/MessageFilters.cs b/Telegrator/Filters/MessageFilters.cs new file mode 100644 index 0000000..a72f03f --- /dev/null +++ b/Telegrator/Filters/MessageFilters.cs @@ -0,0 +1,237 @@ +using System.Text.RegularExpressions; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Filters messages by their . + /// + public class MessageTypeFilter : Filter + { + private readonly MessageType type; + + /// + /// Initializes a new instance of the class. + /// + /// The message type to filter by. + public MessageTypeFilter(MessageType type) => this.type = type; + + /// + public override bool CanPass(FilterExecutionContext context) + => context.Input.Type == type; + } + + /// + /// Filters messages that are automatic forwards. + /// + public class IsAutomaticFormwardMessageFilter : Filter + { + /// + public override bool CanPass(FilterExecutionContext context) + => context.Input.IsAutomaticForward; + } + + /// + /// Filters messages that are sent from offline. + /// + public class IsFromOfflineMessageFilter : Filter + { + /// + public override bool CanPass(FilterExecutionContext context) + => context.Input.IsFromOffline; + } + + /// + /// Filters service messages (e.g., chat events). + /// + public class IsServiceMessageMessageFilter : Filter + { + /// + public override bool CanPass(FilterExecutionContext context) + => context.Input.IsServiceMessage; + } + + /// + /// Filters messages that are topic messages. + /// + public class IsTopicMessageMessageFilter : Filter + { + /// + public override bool CanPass(FilterExecutionContext context) + => context.Input.IsTopicMessage; + } + + /// + /// Filters messages by dice throw value and optionally by dice type. + /// + public class DiceThrowedFilter : Filter + { + private readonly DiceType? Dice; + private readonly int Value; + + /// + /// Initializes a new instance of the class for a specific value. + /// + /// The dice value to filter by. + public DiceThrowedFilter(int value) + { + Value = value; + } + + /// + /// Initializes a new instance of the class for a specific dice type and value. + /// + /// The dice type to filter by. + /// The dice value to filter by. + public DiceThrowedFilter(DiceType diceType, int value) : this(value) => Dice = diceType; + + /// + public override bool CanPass(FilterExecutionContext context) + { + if (context.Input.Dice == null) + return false; + + if (Dice != null && context.Input.Dice.Emoji != GetEmojyForDiceType(Dice)) + return false; + + return context.Input.Dice.Value == Value; + } + + private static string? GetEmojyForDiceType(DiceType? diceType) => diceType switch + { + DiceType.Dice => "🎲", + DiceType.Darts => "🎯", + DiceType.Bowling => "🎳", + DiceType.Basketball => "🏀", + DiceType.Football => "⚽", + DiceType.Casino => "🎰", + _ => null + }; + } + + /// + /// Filters messages by matching their text with a regular expression. + /// + public class MessageRegexFilter : RegexFilterBase + { + /// + /// Initializes a new instance of the class with a pattern and options. + /// + /// The regex pattern. + /// The regex options. + public MessageRegexFilter(string pattern, RegexOptions regexOptions = default) + : base(msg => msg.Text, pattern, regexOptions) { } + + /// + /// Initializes a new instance of the class with a regex object. + /// + /// The regex object. + public MessageRegexFilter(Regex regex) + : base(msg => msg.Text, regex) { } + } + + /// + /// Filters messages that contain a specific entity type, content, offset, or length. + /// + public class MessageHasEntityFilter : Filter + { + private readonly StringComparison _stringComparison = StringComparison.CurrentCulture; + private readonly MessageEntityType? EntityType; + private readonly string? Content; + private readonly int? Offset; + private readonly int? Length; + + /// + /// Gets the entities found in the message that match the filter. + /// + public MessageEntity[] FoundEntities { get; set; } = null!; + + /// + /// Initializes a new instance of the class for a specific entity type. + /// + /// The entity type to filter by. + public MessageHasEntityFilter(MessageEntityType type) + { + EntityType = type; + } + + /// + /// Initializes a new instance of the class for a specific entity type, offset, and length. + /// + /// The entity type to filter by. + /// The offset to filter by. + /// The length to filter by. + public MessageHasEntityFilter(MessageEntityType type, int offset, int? length) + { + EntityType = type; + Offset = offset; + Length = length; + } + + /// + /// Initializes a new instance of the class for a specific entity type and content. + /// + /// The entity type to filter by. + /// The content to filter by. + /// The string comparison to use. + public MessageHasEntityFilter(MessageEntityType type, string content, StringComparison stringComparison = StringComparison.CurrentCulture) + { + EntityType = type; + Content = content; + _stringComparison = stringComparison; + } + + /// + /// Initializes a new instance of the class for a specific entity type, offset, length, and content. + /// + /// The entity type to filter by. + /// The offset to filter by. + /// The length to filter by. + /// The content to filter by. + /// The string comparison to use. + public MessageHasEntityFilter(MessageEntityType type, int offset, int? length, string content, StringComparison stringComparison = StringComparison.CurrentCulture) + { + EntityType = type; + Offset = offset; + Length = length; + Content = content; + _stringComparison = stringComparison; + } + + /// + public override bool CanPass(FilterExecutionContext context) + { + if (context.Input is not { Entities.Length: > 0 }) + return false; + + FoundEntities = context.Input.Entities.Where(entity => FilterEntity(context.Input.Text, entity)).ToArray(); + return FoundEntities.Length != 0; + } + + private bool FilterEntity(string? text, MessageEntity entity) + { + if (EntityType != null && entity.Type != EntityType) + return false; + + if (Offset != null && entity.Offset != Offset) + return false; + + if (Length != null && entity.Length != Length) + return false; + + if (Content != null) + { + if (text is not { Length: > 0 }) + return false; + + if (!text.Substring(entity.Offset, entity.Length).Equals(Content, _stringComparison)) + return false; + } + + return true; + } + } +} diff --git a/Telegrator/Filters/MessageSenderFilters.cs b/Telegrator/Filters/MessageSenderFilters.cs new file mode 100644 index 0000000..0122b75 --- /dev/null +++ b/Telegrator/Filters/MessageSenderFilters.cs @@ -0,0 +1,187 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Abstract base class for filters that operate on message senders. + /// Provides functionality to access and validate the user who sent the message. + /// + public abstract class MessageSenderFilter : Filter + { + /// + /// Gets the user who sent the message. + /// + public User User { get; private set; } = null!; + + /// + /// Determines if the message can pass through the filter by validating + /// that the message has a valid sender. + /// + /// The filter execution context containing the message. + /// True if the message has a valid sender; otherwise, false. + public override bool CanPass(FilterExecutionContext context) + { + User = context.Input.From!; + if (User is not { Id: > 0 }) + return false; + + return CanPassNext(context); + } + + /// + /// Abstract method that must be implemented by derived classes to perform + /// specific filtering logic on the message sender. + /// + /// The filter execution context. + /// True if the sender passes the specific filter criteria; otherwise, false. + protected abstract bool CanPassNext(FilterExecutionContext context); + } + + /// + /// Filter that checks if the message sender has a specific username. + /// + /// The username to check for. + public class FromUsernameFilter(string username) : MessageSenderFilter + { + /// + /// The username to check for. + /// + private readonly string _username = username; + + /// + /// The string comparison type to use for username matching. + /// + private readonly StringComparison _comparison = StringComparison.InvariantCulture; + + /// + /// Initializes a new instance of the class with custom string comparison. + /// + /// The username to check for. + /// The string comparison type to use. + public FromUsernameFilter(string username, StringComparison comparison) + : this(username) => _comparison = comparison; + + /// + /// Checks if the message sender has the specified username. + /// + /// The filter execution context (unused). + /// True if the sender has the specified username; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext context) + => User.Username != null && User.Username.Equals(_username, _comparison); + } + + /// + /// Filter that checks if the message sender has specific first and/or last name. + /// + /// The first name to check for. + /// The last name to check for (optional). + /// The string comparison type to use. + public class FromUserFilter(string firstName, string? lastName, StringComparison comparison) : MessageSenderFilter + { + /// + /// The first name to check for. + /// + private readonly string _firstName = firstName; + + /// + /// The last name to check for (optional). + /// + private readonly string? _lastName = lastName; + + /// + /// The string comparison type to use for name matching. + /// + private readonly StringComparison _comparison = comparison; + + /// + /// Initializes a new instance of the class with first and last name. + /// + /// The first name to check for. + /// The last name to check for. + public FromUserFilter(string firstName, string lastName) + : this(firstName, lastName, StringComparison.InvariantCulture) { } + + /// + /// Initializes a new instance of the class with first name only. + /// + /// The first name to check for. + public FromUserFilter(string firstName) + : this(firstName, null, StringComparison.InvariantCulture) { } + + /// + /// Initializes a new instance of the class with first name and custom comparison. + /// + /// The first name to check for. + /// The string comparison type to use. + public FromUserFilter(string firstName, StringComparison comparison) + : this(firstName, null, comparison) { } + + /// + /// Checks if the message sender has the specified first and/or last name. + /// + /// The filter execution context (unused). + /// True if the sender has the specified name(s); otherwise, false. + protected override bool CanPassNext(FilterExecutionContext context) + { + if (User.LastName != null) + { + if (_lastName == null) + return false; + + if (!_firstName.Equals(User.LastName, _comparison)) + return false; + } + + return User.FirstName.Equals(_firstName, _comparison); + } + } + + /// + /// Filter that checks if the message sender has a specific user ID. + /// + /// The user ID to check for. + public class FromUserIdFilter(long userId) : MessageSenderFilter + { + /// + /// The user ID to check for. + /// + private readonly long _userId = userId; + + /// + /// Checks if the message sender has the specified user ID. + /// + /// The filter execution context (unused). + /// True if the sender has the specified user ID; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => User.Id == _userId; + } + + /// + /// Filter that checks if the message was sent by a bot. + /// + public class FromBotFilter() : MessageSenderFilter + { + /// + /// Checks if the message was sent by a bot. + /// + /// The filter execution context (unused). + /// True if the message was sent by a bot; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => User.IsBot; + } + + /// + /// Filter that checks if the message was sent by a premium user. + /// + public class FromPremiumUserFilter() : MessageSenderFilter + { + /// + /// Checks if the message was sent by a premium user. + /// + /// The filter execution context (unused). + /// True if the message was sent by a premium user; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => User.IsPremium; + } +} diff --git a/Telegrator/Filters/MessageTextFilters.cs b/Telegrator/Filters/MessageTextFilters.cs new file mode 100644 index 0000000..d9e84d1 --- /dev/null +++ b/Telegrator/Filters/MessageTextFilters.cs @@ -0,0 +1,166 @@ +using System; +using System.Linq; +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Abstract base class for filters that operate on message text content. + /// Provides common functionality for extracting and validating message text. + /// + public abstract class MessageTextFilter : Filter + { + /// + /// Gets the current message being processed by the filter. + /// + public Message Message { get; private set; } = null!; + + /// + /// Gets the extracted text content from the current message. + /// + public string Text { get; private set; } = null!; + + /// + /// Determines if the message can pass through the filter by validating the message + /// and extracting its text content for further processing. + /// + /// The filter execution context containing the message update. + /// True if the message is valid and can be processed further; otherwise, false. + public override bool CanPass(FilterExecutionContext context) + { + Message = context.Update.Message!; + if (Message is not { Id: > 0 }) + return false; + + Text = Message.Text ?? string.Empty; + return CanPassNext(context); + } + + /// + /// Abstract method that must be implemented by derived classes to perform + /// specific text-based filtering logic. + /// + /// The filter execution context (unused in this context). + /// True if the text content passes the filter criteria; otherwise, false. + protected abstract bool CanPassNext(FilterExecutionContext _); + } + + /// + /// Filter that checks if the message text starts with a specified content. + /// + /// The content to check if the message text starts with. + /// The string comparison type to use for the check. + public class TextStartsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture) : MessageTextFilter + { + /// + /// The content to check if the message text starts with. + /// + protected readonly string Content = content; + + /// + /// The string comparison type to use for the check. + /// + protected readonly StringComparison Comparison = comparison; + + /// + /// Checks if the message text starts with the specified content using the configured comparison. + /// + /// The filter execution context (unused). + /// True if the text starts with the specified content; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => Text.StartsWith(Content, Comparison); + } + + /// + /// Filter that checks if the message text ends with a specified content. + /// + /// The content to check if the message text ends with. + /// The string comparison type to use for the check. + public class TextEndsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture) : MessageTextFilter + { + /// + /// The content to check if the message text ends with. + /// + protected readonly string Content = content; + + /// + /// The string comparison type to use for the check. + /// + protected readonly StringComparison Comparison = comparison; + + /// + /// Checks if the message text ends with the specified content using the configured comparison. + /// + /// The filter execution context (unused). + /// True if the text ends with the specified content; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => Text.EndsWith(Content, Comparison); + } + + /// + /// Filter that checks if the message text contains a specified content. + /// + /// The content to check if the message text contains. + /// The string comparison type to use for the check. + public class TextContainsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture) : MessageTextFilter + { + /// + /// The content to check if the message text contains. + /// + protected readonly string Content = content; + + /// + /// The string comparison type to use for the check. + /// + protected readonly StringComparison Comparison = comparison; + + /// + /// Checks if the message text contains the specified content using the configured comparison. + /// + /// The filter execution context (unused). + /// True if the text contains the specified content; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => Text.IndexOf(Content, Comparison) >= 0; + } + + /// + /// Filter that checks if the message text equals a specified content. + /// + /// The content to check if the message text equals. + /// The string comparison type to use for the check. + public class TextEqualsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture) : MessageTextFilter + { + /// + /// The content to check if the message text equals. + /// + protected readonly string Content = content; + + /// + /// The string comparison type to use for the check. + /// + protected readonly StringComparison Comparison = comparison; + + /// + /// Checks if the message text equals the specified content using the configured comparison. + /// + /// The filter execution context (unused). + /// True if the text equals the specified content; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => Text.Equals(Content, Comparison); + } + + /// + /// Filter that checks if the message text is not null or empty. + /// + public class TextNotNullOrEmptyFilter() : MessageTextFilter + { + /// + /// Checks if the message text is not null or empty. + /// + /// The filter execution context (unused). + /// True if the text is not null or empty; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => !string.IsNullOrEmpty(Text); + } +} diff --git a/Telegrator/Filters/RegexFilters.cs b/Telegrator/Filters/RegexFilters.cs new file mode 100644 index 0000000..343c824 --- /dev/null +++ b/Telegrator/Filters/RegexFilters.cs @@ -0,0 +1,58 @@ +using System.Text.RegularExpressions; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Base class for filters that use regular expressions to match text in updates. + /// + /// The type of the input for the filter. + public abstract class RegexFilterBase : Filter where T : class + { + private readonly Func getString; + private readonly Regex regex; + + /// + /// Gets the collection of matches found by the regex. + /// + public MatchCollection Matches { get; private set; } = null!; + + /// + /// Initializes a new instance of the class with a regex object. + /// + /// Function to extract the string to match from the input. + /// The regex object to use for matching. + protected RegexFilterBase(Func getString, Regex regex) + { + this.getString = getString; + this.regex = regex; + } + + /// + /// Initializes a new instance of the class with a pattern and options. + /// + /// Function to extract the string to match from the input. + /// The regex pattern. + /// The regex options. + protected RegexFilterBase(Func getString, string pattern, RegexOptions regexOptions = default) + { + this.getString = getString; + regex = new Regex(pattern, regexOptions); + } + + /// + /// Determines whether the regex matches the text extracted from the input. + /// + /// The filter execution context. + /// True if the regex matches; otherwise, false. + public override bool CanPass(FilterExecutionContext context) + { + string? text = getString.Invoke(context.Input); + if (string.IsNullOrEmpty(text)) + return false; + + Matches = regex.Matches(text); + return Matches.Count > 0; + } + } +} diff --git a/Telegrator/Filters/RepliedMentionedFilter.cs b/Telegrator/Filters/RepliedMentionedFilter.cs new file mode 100644 index 0000000..e64f817 --- /dev/null +++ b/Telegrator/Filters/RepliedMentionedFilter.cs @@ -0,0 +1,56 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Filter that checks if a replied message contains a mention of the bot or a specific user. + /// Requires a to be applied first to identify mention entities. + /// + public class RepliedMentionedFilter : RepliedMessageFilter + { + /// + /// The username to check for in the mention (null means check for bot's username). + /// + private readonly string? Mention; + + /// + /// Initializes a new instance of the class that checks for bot mentions. + /// + /// The depth of reply chain to traverse (default: 1). + public RepliedMentionedFilter(int replyDepth = 1) : base(replyDepth) + { + Mention = null; + } + + /// + /// Initializes a new instance of the class that checks for specific user mentions. + /// + /// The username to check for in the mention. + /// The depth of reply chain to traverse (default: 1). + public RepliedMentionedFilter(string mention, int replyDepth = 1) : base(replyDepth) + { + Mention = mention; + } + + /// + /// Checks if the replied message contains a mention of the specified user or bot. + /// This filter requires a to be applied first + /// to identify mention entities in the replied message. + /// + /// The filter execution context containing the message and completed filters. + /// True if the replied message contains the specified mention; otherwise, false. + /// Thrown when the bot username is null and no specific mention is provided. + protected override bool CanPassNext(FilterExecutionContext context) + { + if (Reply.Text == null) + return false; + + string userName = Mention ?? context.BotInfo.User.Username ?? throw new ArgumentNullException(nameof(context), "RepliedMentionedFilter requires BotInfo to be initialized"); + MessageEntity entity = context.CompletedFilters.Get(0).FoundEntities.ElementAt(0); + + string mention = Reply.Text.Substring(entity.Offset + 1, entity.Length - 1); + return userName == mention; + } + } +} diff --git a/Telegrator/Filters/RepliedMessageChatFilters.cs b/Telegrator/Filters/RepliedMessageChatFilters.cs new file mode 100644 index 0000000..aa3838e --- /dev/null +++ b/Telegrator/Filters/RepliedMessageChatFilters.cs @@ -0,0 +1,211 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Base class for filters that operate on the chat of a replied message (the message being replied to). + /// The replyDepth parameter determines how many levels up the reply chain to search for the target message. + /// + public abstract class RepliedMessageChatFilter : RepliedMessageFilter + { + /// + /// Gets the chat of the replied message (the message being replied to at the specified depth). + /// + public Chat Chat { get; private set; } = null!; + + /// + /// Initializes a new instance of the class. + /// + /// The reply depth to search up the reply chain for the target message. + protected RepliedMessageChatFilter(int replyDepth = 1) : base(replyDepth) { } + + /// + public override bool CanPass(FilterExecutionContext context) + { + if (!CanPassReply(context)) + return false; + + Chat = Reply.Chat; + return CanPassNext(context); + } + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose chat is a forum. + /// + public class RepliedMessageChatIsForumFilter : RepliedMessageChatFilter + { + /// + /// Initializes a new instance of the class. + /// + /// The reply depth to search up the reply chain for the target message. + public RepliedMessageChatIsForumFilter(int replyDepth = 1) + : base(replyDepth) { } + + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Chat.IsForum; + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose chat ID matches the specified value. + /// + public class RepliedMessageChatIdFilter : RepliedMessageChatFilter + { + private readonly long Id; + + /// + /// Initializes a new instance of the class. + /// + /// The chat ID to match. + /// The reply depth to search up the reply chain for the target message. + public RepliedMessageChatIdFilter(long id, int replyDepth = 1) : base(replyDepth) => Id = id; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Chat.Id == Id; + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose chat type matches the specified value. + /// + public class RepliedMessageChatTypeFilter : RepliedMessageChatFilter + { + private readonly ChatType Type; + + /// + /// Initializes a new instance of the class. + /// + /// The chat type to match. + /// The reply depth to search up the reply chain for the target message. + public RepliedMessageChatTypeFilter(ChatType type, int replyDepth = 1) : base(replyDepth) => Type = type; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Chat.Type == Type; + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose chat title matches the specified value. + /// + public class RepliedMessageChatTitleFilter : RepliedMessageChatFilter + { + private readonly string? Title; + private readonly StringComparison Comparison = StringComparison.InvariantCulture; + + /// + /// Initializes a new instance of the class. + /// + /// The chat title to match. + /// The reply depth to search up the reply chain for the target message. + public RepliedMessageChatTitleFilter(string? title, int replyDepth = 1) : base(replyDepth) => Title = title; + + /// + /// Initializes a new instance of the class with a specific string comparison. + /// + /// The chat title to match. + /// The string comparison to use. + /// The reply depth to search up the reply chain for the target message. + public RepliedMessageChatTitleFilter(string? title, StringComparison comparison, int replyDepth = 1) + : this(title, replyDepth) => Comparison = comparison; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + { + if (Chat.Title == null) + return false; + return Chat.Title.Equals(Title, Comparison); + } + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose chat username matches the specified value. + /// + public class RepliedMessageChatUsernameFilter : RepliedMessageChatFilter + { + private readonly string? UserName; + private readonly StringComparison Comparison = StringComparison.InvariantCulture; + + /// + /// Initializes a new instance of the class. + /// + /// The chat username to match. + /// The reply depth to search up the reply chain for the target message. + public RepliedMessageChatUsernameFilter(string? userName, int replyDepth = 1) : base(replyDepth) => UserName = userName; + + /// + /// Initializes a new instance of the class with a specific string comparison. + /// + /// The chat username to match. + /// The string comparison to use. + /// The reply depth to search up the reply chain for the target message. + public RepliedMessageChatUsernameFilter(string? userName, StringComparison comparison, int replyDepth = 1) + : this(userName, replyDepth) => Comparison = comparison; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + { + if (Chat.Username == null) + return false; + return Chat.Username.Equals(UserName, Comparison); + } + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose chat first and/or last name matches the specified values. + /// + public class RepliedMessageChatNameFilter : RepliedMessageChatFilter + { + private readonly string? FirstName; + private readonly string? LastName; + private readonly StringComparison Comparison = StringComparison.InvariantCulture; + + /// + /// Initializes a new instance of the class. + /// + /// The chat first name to match. + /// The chat last name to match. + /// The reply depth to search up the reply chain for the target message. + public RepliedMessageChatNameFilter(string? firstName, string? lastName, int replyDepth = 1) : base(replyDepth) + { + FirstName = firstName; + LastName = lastName; + } + + /// + /// Initializes a new instance of the class with a specific string comparison. + /// + /// The chat first name to match. + /// The chat last name to match. + /// The string comparison to use. + /// The reply depth to search up the reply chain for the target message. + public RepliedMessageChatNameFilter(string? firstName, string? lastName, StringComparison comparison, int replyDepth = 1) + : this(firstName, lastName, replyDepth) => Comparison = comparison; + + /// + protected override bool CanPassNext(FilterExecutionContext _) + { + if (LastName != null) + { + if (Chat.LastName == null) + return false; + + if (Chat.LastName.Equals(LastName, Comparison)) + return false; + } + + if (FirstName != null) + { + if (Chat.FirstName == null) + return false; + + if (Chat.FirstName.Equals(FirstName, Comparison)) + return false; + } + + return true; + } + } +} diff --git a/Telegrator/Filters/RepliedMessageFilters.cs b/Telegrator/Filters/RepliedMessageFilters.cs new file mode 100644 index 0000000..17e1300 --- /dev/null +++ b/Telegrator/Filters/RepliedMessageFilters.cs @@ -0,0 +1,383 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Abstract base class for filters that operate on replied messages. + /// Provides functionality to traverse reply chains and access replied message content. + /// + /// The depth of reply chain to traverse (default: 1). + public abstract class RepliedMessageFilter(int replyDepth = 1) : Filter + { + /// + /// Gets the replied message at the specified depth in the reply chain. + /// + public Message Reply { get; private set; } = null!; + + /// + /// Gets the depth of reply chain traversal. + /// + public int ReplyDepth { get; private set; } = replyDepth; + + /// + /// Validates that the message has a valid reply chain at the specified depth. + /// + /// The filter execution context containing the message. + /// True if the reply chain is valid at the specified depth; otherwise, false. + protected bool CanPassReply(FilterExecutionContext context) + { + Message reply = context.Input; + for (int i = ReplyDepth; i > 0; i--) + { + if (reply.ReplyToMessage is not { Id: > 0 } replyMessage) + return false; + + reply = replyMessage; + } + + Reply = reply; + return true; + } + + /// + /// Determines if the message can pass through the filter by first validating + /// the reply chain and then applying specific filter logic. + /// + /// The filter execution context containing the message. + /// True if the message passes both reply validation and specific filter criteria; otherwise, false. + public override bool CanPass(FilterExecutionContext context) + { + if (!CanPassReply(context)) + return false; + + return CanPassNext(context); + } + + /// + /// Abstract method that must be implemented by derived classes to perform + /// specific filtering logic on the replied message. + /// + /// The filter execution context. + /// True if the replied message passes the specific filter criteria; otherwise, false. + protected abstract bool CanPassNext(FilterExecutionContext context); + } + + /// + /// Filter that checks if a message is a reply to another message at the specified depth. + /// + /// The depth of reply chain to traverse (default: 1). + public class MessageRepliedFilter(int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// Always returns true if the reply chain is valid at the specified depth. + /// + /// The filter execution context (unused). + /// True if the reply chain is valid; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext context) + => true; + } + + /// + /// Filter that checks if the replied message was sent by the bot itself. + /// + /// The depth of reply chain to traverse (default: 1). + public class MeRepliedFilter(int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// Checks if the replied message was sent by the bot. + /// + /// The filter execution context containing bot information. + /// True if the replied message was sent by the bot; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext context) + => context.BotInfo.User == Reply.From; + } + + /// + /// Filter that checks if the replied message has non-empty text content. + /// + /// The depth of reply chain to traverse (default: 1). + public class RepliedTextNotNullOrEmptyFilter(int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// Checks if the replied message text is not null or empty. + /// + /// The filter execution context (unused). + /// True if the replied message has non-empty text; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => !string.IsNullOrEmpty(Reply.Text); + } + + /// + /// Filter that checks if the replied message is of a specific type. + /// + /// The message type to check for. + /// The depth of reply chain to traverse (default: 1). + public class RepliedMessageTypeFilter(MessageType type, int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// Checks if the replied message is of the specified type. + /// + /// The filter execution context (unused). + /// True if the replied message is of the specified type; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => Reply.Type == type; + } + + /// + /// Filter that checks if the replied message is an automatic forward. + /// + /// The depth of reply chain to traverse (default: 1). + public class RepliedIsAutomaticFormwardMessageFilter(int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// Checks if the replied message is an automatic forward. + /// + /// The filter execution context (unused). + /// True if the replied message is an automatic forward; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => Reply.IsAutomaticForward; + } + + /// + /// Filter that checks if the replied message is from an offline user. + /// + /// The depth of reply chain to traverse (default: 1). + public class RepliedIsFromOfflineMessageFilter(int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// Checks if the replied message is from an offline user. + /// + /// The filter execution context (unused). + /// True if the replied message is from an offline user; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => Reply.IsFromOffline; + } + + /// + /// Filter that checks if the replied message is a service message. + /// + /// The depth of reply chain to traverse (default: 1). + public class RepliedIsServiceMessageMessageFilter(int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// Checks if the replied message is a service message. + /// + /// The filter execution context (unused). + /// True if the replied message is a service message; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => Reply.IsServiceMessage; + } + + /// + /// Filter that checks if the replied message is a topic message. + /// + /// The depth of reply chain to traverse (default: 1). + public class RepliedIsTopicMessageMessageFilter(int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// Checks if the replied message is a topic message. + /// + /// The filter execution context (unused). + /// True if the replied message is a topic message; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => Reply.IsTopicMessage; + } + + /// + /// Filter that checks if the replied message contains a dice with a specific value. + /// + /// The dice value to check for. + /// The depth of reply chain to traverse (default: 1). + public class RepliedDiceThrowedFilter(int value, int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// The dice type to check for (optional). + /// + private readonly DiceType? Dice = null; + + /// + /// The dice value to check for. + /// + private readonly int Value = value; + + /// + /// Initializes a new instance of the class with a specific dice type and value. + /// + /// The dice type to check for. + /// The dice value to check for. + /// The depth of reply chain to traverse (default: 1). + public RepliedDiceThrowedFilter(DiceType diceType, int value, int replyDepth = 1) + : this(value, replyDepth) => Dice = diceType; + + /// + /// Checks if the replied message contains a dice with the specified value and optionally the specified type. + /// + /// The filter execution context containing the message. + /// True if the replied message contains a dice with the specified criteria; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext context) + { + if (context.Input.Dice == null) + return false; + + if (Dice != null && context.Input.Dice.Emoji != GetEmojyForDiceType(Dice)) + return false; + + return context.Input.Dice.Value == Value; + } + + /// + /// Gets the emoji representation for a specific dice type. + /// + /// The dice type to get the emoji for. + /// The emoji string for the dice type, or null if not found. + private static string? GetEmojyForDiceType(DiceType? diceType) => diceType switch + { + DiceType.Dice => "🎲", + DiceType.Darts => "🎯", + DiceType.Bowling => "🎳", + DiceType.Basketball => "🏀", + DiceType.Football => "⚽", + DiceType.Casino => "🎰", + _ => null + }; + } + + /// + /// Filter that checks if the replied message contains specific message entities. + /// + /// The depth of reply chain to traverse (default: 1). + public class RepliedMessageHasEntityFilter(int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// The string comparison type to use for content matching. + /// + private readonly StringComparison _stringComparison = StringComparison.CurrentCulture; + + /// + /// The entity type to filter by (optional). + /// + private readonly MessageEntityType? EntityType; + + /// + /// The content to match in the entity (optional). + /// + private readonly string? Content; + + /// + /// The offset position to check (optional). + /// + private readonly int? Offset; + + /// + /// The length to check (optional). + /// + private readonly int? Length; + + /// + /// Gets the found entities that match the filter criteria. + /// + public MessageEntity[]? FoundEntities { get; set; } = null!; + + /// + /// Initializes a new instance of the class with a specific entity type. + /// + /// The entity type to filter by. + /// The depth of reply chain to traverse (default: 1). + public RepliedMessageHasEntityFilter(MessageEntityType type, int replyDepth = 1) : this(replyDepth) + { + EntityType = type; + } + + /// + /// Initializes a new instance of the class with position criteria. + /// + /// The entity type to filter by. + /// The offset position to check. + /// The length to check (optional). + /// The depth of reply chain to traverse (default: 1). + public RepliedMessageHasEntityFilter(MessageEntityType type, int offset, int? length, int replyDepth = 1) : this(replyDepth) + { + EntityType = type; + Offset = offset; + Length = length; + } + + /// + /// Initializes a new instance of the class with content criteria. + /// + /// The entity type to filter by. + /// The content to match in the entity. + /// The string comparison type to use. + /// The depth of reply chain to traverse (default: 1). + public RepliedMessageHasEntityFilter(MessageEntityType type, string content, StringComparison stringComparison = StringComparison.CurrentCulture, int replyDepth = 1) : this(replyDepth) + { + EntityType = type; + Content = content; + _stringComparison = stringComparison; + } + + /// + /// Initializes a new instance of the class with all criteria. + /// + /// The entity type to filter by. + /// The offset position to check. + /// The length to check (optional). + /// The content to match in the entity. + /// The string comparison type to use. + /// The depth of reply chain to traverse (default: 1). + public RepliedMessageHasEntityFilter(MessageEntityType type, int offset, int? length, string content, StringComparison stringComparison = StringComparison.CurrentCulture, int replyDepth = 1) : this(replyDepth) + { + EntityType = type; + Offset = offset; + Length = length; + Content = content; + _stringComparison = stringComparison; + } + + /// + /// Checks if the replied message contains entities that match the specified criteria. + /// + /// The filter execution context containing the message. + /// True if matching entities are found; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext context) + { + if (context.Input is not { Entities.Length: > 0 }) + return false; + + FoundEntities = context.Input.Entities.Where(entity => FilterEntity(context.Input.Text, entity)).ToArray(); + return FoundEntities.Length != 0; + } + + /// + /// Filters an entity based on the specified criteria. + /// + /// The message text containing the entity. + /// The entity to filter. + /// True if the entity matches all specified criteria; otherwise, false. + private bool FilterEntity(string? text, MessageEntity entity) + { + if (EntityType != null && entity.Type != EntityType) + return false; + + if (Offset != null && entity.Offset != Offset) + return false; + + if (Length != null && entity.Length != Length) + return false; + + if (Content != null) + { + if (text is not { Length: > 0 }) + return false; + + if (!text.Substring(entity.Offset, entity.Length).Equals(Content, _stringComparison)) + return false; + } + + return true; + } + } +} diff --git a/Telegrator/Filters/RepliedMessageSenderFilters.cs b/Telegrator/Filters/RepliedMessageSenderFilters.cs new file mode 100644 index 0000000..1813753 --- /dev/null +++ b/Telegrator/Filters/RepliedMessageSenderFilters.cs @@ -0,0 +1,192 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Abstract base class for filters that operate on the sender of replied messages. + /// Provides functionality to access and validate the user who sent the replied message. + /// + /// The depth of reply chain to traverse (default: 1). + public abstract class RepliedMessageSenderFilter(int replyDepth = 1) : RepliedMessageFilter(replyDepth) + { + /// + /// Gets the user who sent the replied message. + /// + public User User { get; private set; } = null!; + + /// + /// Determines if the message can pass through the filter by validating the reply chain + /// and ensuring the replied message has a valid sender. + /// + /// The filter execution context containing the message. + /// True if the reply chain is valid and has a sender; otherwise, false. + public override bool CanPass(FilterExecutionContext context) + { + if (!CanPassReply(context)) + return false; + + if (Reply.From is not { Id: > 0 } from) + return false; + + User = from; + return CanPassNext(context); + } + } + + /// + /// Filter that checks if the replied message sender has a specific username. + /// + /// The username to check for. + /// The depth of reply chain to traverse (default: 1). + public class RepliedUsernameFilter(string username, int replyDepth = 1) : RepliedMessageSenderFilter(replyDepth) + { + /// + /// The username to check for. + /// + private readonly string _username = username; + + /// + /// The string comparison type to use for username matching. + /// + private readonly StringComparison _comparison = StringComparison.InvariantCulture; + + /// + /// Initializes a new instance of the class with custom string comparison. + /// + /// The username to check for. + /// The string comparison type to use. + /// The depth of reply chain to traverse (default: 1). + public RepliedUsernameFilter(string username, StringComparison comparison, int replyDepth = 1) + : this(username, replyDepth) => _comparison = comparison; + + /// + /// Checks if the replied message sender has the specified username. + /// + /// The filter execution context (unused). + /// True if the sender has the specified username; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext context) + => User.Username != null && User.Username.Equals(_username, _comparison); + } + + /// + /// Filter that checks if the replied message sender has specific first and/or last name. + /// + /// The first name to check for. + /// The last name to check for (optional). + /// The string comparison type to use. + /// The depth of reply chain to traverse (default: 1). + public class RepliedUserFilter(string firstName, string? lastName, StringComparison comparison, int replyDepth = 1) : RepliedMessageSenderFilter(replyDepth) + { + /// + /// The first name to check for. + /// + private readonly string _firstName = firstName; + + /// + /// The last name to check for (optional). + /// + private readonly string? _lastName = lastName; + + /// + /// The string comparison type to use for name matching. + /// + private readonly StringComparison _comparison = comparison; + + /// + /// Initializes a new instance of the class with first and last name. + /// + /// The first name to check for. + /// The last name to check for. + /// The depth of reply chain to traverse (default: 1). + public RepliedUserFilter(string firstName, string lastName, int replyDepth = 1) + : this(firstName, lastName, StringComparison.InvariantCulture, replyDepth) { } + + /// + /// Initializes a new instance of the class with first name only. + /// + /// The first name to check for. + /// The depth of reply chain to traverse (default: 1). + public RepliedUserFilter(string firstName, int replyDepth = 1) + : this(firstName, null, StringComparison.InvariantCulture, replyDepth) { } + + /// + /// Initializes a new instance of the class with first name and custom comparison. + /// + /// The first name to check for. + /// The string comparison type to use. + /// The depth of reply chain to traverse (default: 1). + public RepliedUserFilter(string firstName, StringComparison comparison, int replyDepth = 1) + : this(firstName, null, comparison, replyDepth) { } + + /// + /// Checks if the replied message sender has the specified first and/or last name. + /// + /// The filter execution context (unused). + /// True if the sender has the specified name(s); otherwise, false. + protected override bool CanPassNext(FilterExecutionContext context) + { + if (User.LastName != null) + { + if (_lastName == null) + return false; + + if (!_firstName.Equals(User.LastName, _comparison)) + return false; + } + + return User.FirstName.Equals(_firstName, _comparison); + } + } + + /// + /// Filter that checks if the replied message sender has a specific user ID. + /// + /// The user ID to check for. + /// The depth of reply chain to traverse (default: 1). + public class RepliedUserIdFilter(long userId, int replyDepth = 1) : RepliedMessageSenderFilter(replyDepth) + { + /// + /// The user ID to check for. + /// + private readonly long _userId = userId; + + /// + /// Checks if the replied message sender has the specified user ID. + /// + /// The filter execution context (unused). + /// True if the sender has the specified user ID; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => User.Id == _userId; + } + + /// + /// Filter that checks if the replied message was sent by a bot. + /// + /// The depth of reply chain to traverse (default: 1). + public class ReplyFromBotFilter(int replyDepth = 1) : RepliedMessageSenderFilter(replyDepth) + { + /// + /// Checks if the replied message was sent by a bot. + /// + /// The filter execution context (unused). + /// True if the replied message was sent by a bot; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => User.IsBot; + } + + /// + /// Filter that checks if the replied message was sent by a premium user. + /// + /// The depth of reply chain to traverse (default: 1). + public class ReplyFromPremiumUserFilter(int replyDepth = 1) : RepliedMessageSenderFilter(replyDepth) + { + /// + /// Checks if the replied message was sent by a premium user. + /// + /// The filter execution context (unused). + /// True if the replied message was sent by a premium user; otherwise, false. + protected override bool CanPassNext(FilterExecutionContext _) + => User.IsPremium; + } +} diff --git a/Telegrator/Filters/RepliedMessageTextFilters.cs b/Telegrator/Filters/RepliedMessageTextFilters.cs new file mode 100644 index 0000000..5a5af30 --- /dev/null +++ b/Telegrator/Filters/RepliedMessageTextFilters.cs @@ -0,0 +1,123 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + /// + /// Base class for filters that operate on the text of a replied message (the message being replied to). + /// The replyDepth parameter determines how many levels up the reply chain to search for the target message. + /// + public abstract class RepliedMessageTextFilters : RepliedMessageFilter + { + /// + /// Gets the text of the replied message (the message being replied to at the specified depth). + /// + public string Text { get; private set; } = null!; + + /// + /// The content to match in the replied message. + /// + protected readonly string Content; + + /// + /// The string comparison to use for matching. + /// + protected readonly StringComparison Comparison; + + /// + /// Initializes a new instance of the class. + /// + /// The content to match. + /// The string comparison to use. + /// The reply depth to search up the reply chain for the target message. + protected RepliedMessageTextFilters(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1) + : base(replyDepth) + { + Content = content; + Comparison = comparison; + } + + /// + public override bool CanPass(FilterExecutionContext context) + { + Text = Reply.Text ?? string.Empty; + return CanPassNext(context); + } + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose text starts with the specified content. + /// + public class RepliedTextStartsWithFilter : RepliedMessageTextFilters + { + /// + /// Initializes a new instance of the class. + /// + /// The content to match. + /// The string comparison to use. + /// The reply depth to search up the reply chain for the target message. + public RepliedTextStartsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1) + : base(content, comparison, replyDepth) { } + + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Text.StartsWith(Content, Comparison); + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose text ends with the specified content. + /// + public class RepliedTextEndsWithFilter : RepliedMessageTextFilters + { + /// + /// Initializes a new instance of the class. + /// + /// The content to match. + /// The string comparison to use. + /// The reply depth to search up the reply chain for the target message. + public RepliedTextEndsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1) + : base(content, comparison, replyDepth) { } + + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Text.EndsWith(Content, Comparison); + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose text contains the specified content. + /// + public class RepliedTextContainsFilter : RepliedMessageTextFilters + { + /// + /// Initializes a new instance of the class. + /// + /// The content to match. + /// The string comparison to use. + /// The reply depth to search up the reply chain for the target message. + public RepliedTextContainsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1) + : base(content, comparison, replyDepth) { } + + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Text.IndexOf(Content, Comparison) >= 0; + } + + /// + /// Filters replied messages (the message being replied to at the specified depth) whose text equals the specified content. + /// + public class RepliedTextEqualsFilter : RepliedMessageTextFilters + { + /// + /// Initializes a new instance of the class. + /// + /// The content to match. + /// The string comparison to use. + /// The reply depth to search up the reply chain for the target message. + public RepliedTextEqualsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1) + : base(content, comparison, replyDepth) { } + + /// + protected override bool CanPassNext(FilterExecutionContext _) + => Text.Equals(Content, Comparison); + } +} diff --git a/Telegrator/Filters/RepliedToMeFilter.cs b/Telegrator/Filters/RepliedToMeFilter.cs new file mode 100644 index 0000000..892e774 --- /dev/null +++ b/Telegrator/Filters/RepliedToMeFilter.cs @@ -0,0 +1,16 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.Filters +{ + public class RepliedToMeFilter : RepliedMessageFilter + { + protected override bool CanPassNext(FilterExecutionContext context) + { + if (Reply.From == null) + return false; + + return Reply.From.Id == (context.BotInfo?.User.Id ?? throw new ArgumentNullException(nameof(context), "MentionedFilter requires BotInfo to be initialized")); + } + } +} diff --git a/Telegrator/Filters/StateKeyFilter.cs b/Telegrator/Filters/StateKeyFilter.cs new file mode 100644 index 0000000..3b64322 --- /dev/null +++ b/Telegrator/Filters/StateKeyFilter.cs @@ -0,0 +1,31 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Filters +{ + /// + /// Filters updates by comparing a resolved state key with a target key. + /// + /// The type of the key used for state resolution. + public class StateKeyFilter : Filter where TKey : IEquatable + { + private readonly IStateKeyResolver KeyResolver; + private readonly TKey TargetKey; + + /// + /// Initializes a new instance of the class. + /// + /// The key resolver to extract the key from the update. + /// The target key to compare with. + public StateKeyFilter(IStateKeyResolver keyResolver, TKey targetKey) + { + KeyResolver = keyResolver; + TargetKey = targetKey; + } + + /// + public override bool CanPass(FilterExecutionContext context) + => KeyResolver.ResolveKey(context.Input).Equals(TargetKey); + } +} diff --git a/Telegrator/GlobalSuppressions.cs b/Telegrator/GlobalSuppressions.cs new file mode 100644 index 0000000..79b8605 --- /dev/null +++ b/Telegrator/GlobalSuppressions.cs @@ -0,0 +1,10 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0290")] +[assembly: SuppressMessage("Style", "IDE0090")] +[assembly: SuppressMessage("Style", "IDE0057")] diff --git a/Telegrator/Handlers/AbstractHandlerContainer.cs b/Telegrator/Handlers/AbstractHandlerContainer.cs new file mode 100644 index 0000000..b02dc40 --- /dev/null +++ b/Telegrator/Handlers/AbstractHandlerContainer.cs @@ -0,0 +1,62 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Handlers +{ + /// + /// Container class that holds the context and data for handler execution. + /// Provides access to the update, client, filters, and other execution context. + /// + /// The type of update being handled. + public class AbstractHandlerContainer : IAbstractHandlerContainer where TUpdate : class + { + private readonly TUpdate _actualUpdate; + private readonly Update _handlingUpdate; + private readonly ITelegramBotClient _client; + private readonly Dictionary _extraData; + private readonly CompletedFiltersList _completedFilters; + private readonly IAwaitingProvider _awaitingProvider; + + /// + /// Gets the actual update object of type TUpdate. + /// + public TUpdate ActualUpdate => _actualUpdate; + + /// + public Update HandlingUpdate => _handlingUpdate; + + /// + public ITelegramBotClient Client => _client; + + /// + public Dictionary ExtraData => _extraData; + + /// + public CompletedFiltersList CompletedFilters => _completedFilters; + + /// + public IAwaitingProvider AwaitingProvider => _awaitingProvider; + + /// + IAwaitingProvider IHandlerContainer.AwaitingProvider => AwaitingProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The awaiting provider for managing async operations. + /// The handler information containing execution context. + public AbstractHandlerContainer(IAwaitingProvider awaitingProvider, DescribedHandlerInfo handlerInfo) + { + _actualUpdate = handlerInfo.HandlingUpdate.GetActualUpdateObject(); + _handlingUpdate = handlerInfo.HandlingUpdate; + _client = handlerInfo.Client; + _extraData = handlerInfo.ExtraData; + _completedFilters = handlerInfo.CompletedFilters; + _awaitingProvider = awaitingProvider; + } + } +} diff --git a/Telegrator/Handlers/AnyUpdateHandler.cs b/Telegrator/Handlers/AnyUpdateHandler.cs new file mode 100644 index 0000000..39b5328 --- /dev/null +++ b/Telegrator/Handlers/AnyUpdateHandler.cs @@ -0,0 +1,32 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Attributes; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; + +namespace Telegrator.Handlers +{ + /// + /// Attribute that marks a handler to process any type of update. + /// This handler will be triggered for all incoming updates regardless of their type. + /// + /// The maximum number of concurrent executions allowed (default: -1 for unlimited). + public class AnyUpdateHandlerAttribute(int concurrency = -1) : UpdateHandlerAttribute(UpdateType.Unknown, concurrency) + { + /// + /// Always returns true, allowing any update to pass through this filter. + /// + /// The filter execution context (unused). + /// Always returns true to allow any update. + public override bool CanPass(FilterExecutionContext context) => true; + } + + /// + /// Abstract base class for handlers that can process any type of update. + /// Provides a foundation for creating handlers that respond to all incoming updates. + /// + public abstract class AnyUpdateHandler() : AbstractUpdateHandler(UpdateType.Unknown) + { + + } +} diff --git a/Telegrator/Handlers/Building/AwaiterHandler.cs b/Telegrator/Handlers/Building/AwaiterHandler.cs new file mode 100644 index 0000000..3912aa8 --- /dev/null +++ b/Telegrator/Handlers/Building/AwaiterHandler.cs @@ -0,0 +1,69 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Handlers.Building +{ + /// + /// Internal handler used for awaiting specific update types. + /// Provides synchronization mechanism for waiting for updates of a particular type. + /// + /// The type of update this awaiter handler waits for. + internal class AwaiterHandler(UpdateType handlingUpdateType) : UpdateHandlerBase(handlingUpdateType), IHandlerContainerFactory, IDisposable + { + /// + /// Manual reset event used for synchronization. + /// + private ManualResetEventSlim ResetEvent = new ManualResetEventSlim(false); + + /// + /// Gets the update that triggered this awaiter handler. + /// + public Update HandlingUpdate { get; private set; } = null!; + + /// + /// Waits for the specified update type to be received. + /// + /// The cancellation token to cancel the wait operation. + public void Wait(CancellationToken cancellationToken) + { + ResetEvent.Reset(); + ResetEvent.Wait(cancellationToken); + } + + /// + /// Creates a handler container for this awaiter handler. + /// + /// The awaiting provider (unused). + /// The handler information containing the update. + /// An empty handler container. + public IHandlerContainer CreateContainer(IAwaitingProvider _, DescribedHandlerInfo describedHandler) + { + HandlingUpdate = describedHandler.HandlingUpdate; + return new EmptyHandlerContainer(); + } + + /// + /// Executes the awaiter handler by setting the reset event. + /// + /// The handler container (unused). + /// The cancellation token (unused). + /// A completed task. + protected override Task ExecuteInternal(IHandlerContainer container, CancellationToken cancellation) + { + ResetEvent.Set(); + return Task.CompletedTask; + } + + /// + /// Disposes of the reset event. + /// + public void Dispose() + { + ResetEvent.Dispose(); + ResetEvent = null!; + } + } +} diff --git a/Telegrator/Handlers/Building/AwaiterHandlerBuilder.cs b/Telegrator/Handlers/Building/AwaiterHandlerBuilder.cs new file mode 100644 index 0000000..7789fe8 --- /dev/null +++ b/Telegrator/Handlers/Building/AwaiterHandlerBuilder.cs @@ -0,0 +1,74 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Filters; +using Telegrator.StateKeeping; +using Telegrator.Handlers.Building.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Handlers.Building +{ + /// + /// Builder class for creating awaiter handlers that can wait for specific update types. + /// Provides fluent API for configuring filters, state keepers, and other handler properties. + /// + /// The type of update to await. + public class AwaiterHandlerBuilder : HandlerBuilderBase, IAwaiterHandlerBuilder where TUpdate : class + { + /// + /// The awaiting provider for managing handler registration. + /// + private readonly IAwaitingProvider HandlerProvider; + + /// + /// The update that triggered the awaiter creation. + /// + private readonly Update HandlingUpdate; + + /// + /// Initializes a new instance of the class. + /// + /// The type of update to await. + /// The update that triggered the awaiter creation. + /// The awaiting provider for managing handler registration. + /// Thrown when the update type is not valid for TUpdate. + public AwaiterHandlerBuilder(UpdateType updateType, Update handlingUpdate, IAwaitingProvider handlerProvider) : base(typeof(AwaiterHandler), updateType, null) + { + if (!updateType.IsValidUpdateObject()) + throw new Exception(); + + HandlerProvider = handlerProvider; + HandlingUpdate = handlingUpdate; + } + + /// + /// Awaits for an update of the specified type using the default sender ID resolver. + /// + /// The cancellation token to cancel the wait operation. + /// The awaited update of type TUpdate. + public async Task Await(CancellationToken cancellationToken = default) + => await Await(new SenderIdResolver(), cancellationToken); + + /// + /// Awaits for an update of the specified type using a custom state key resolver. + /// + /// The state key resolver to use for filtering updates. + /// The cancellation token to cancel the wait operation. + /// The awaited update of type TUpdate. + public async Task Await(IStateKeyResolver keyResolver, CancellationToken cancellationToken = default) + { + Filters.Add(new StateKeyFilter(keyResolver, keyResolver.ResolveKey(HandlingUpdate))); + AwaiterHandler handlerInstance = new AwaiterHandler(UpdateType); + HandlerDescriptor descriptor = BuildImplicitDescriptor(handlerInstance); + + using (HandlerProvider.UseHandler(descriptor)) + { + handlerInstance.Wait(cancellationToken); + } + + await Task.CompletedTask; + return handlerInstance.HandlingUpdate.GetActualUpdateObject(); + } + } +} diff --git a/Telegrator/Handlers/Building/BuildedAbstractHandler.cs b/Telegrator/Handlers/Building/BuildedAbstractHandler.cs new file mode 100644 index 0000000..ff926cd --- /dev/null +++ b/Telegrator/Handlers/Building/BuildedAbstractHandler.cs @@ -0,0 +1,37 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Handlers.Components; + +namespace Telegrator.Handlers.Building +{ + /// + /// Internal handler class that wraps a delegate action for execution. + /// Used for dynamically created handlers that execute custom actions. + /// + /// The type of update being handled. + internal class BuildedAbstractHandler : AbstractUpdateHandler where TUpdate : class + { + /// + /// The delegate action to execute when the handler is invoked. + /// + private readonly AbstractHandlerAction HandlerAction; + + /// + /// Initializes a new instance of the class. + /// + /// The type of update this handler processes. + /// The delegate action to execute. + public BuildedAbstractHandler(UpdateType handlingUpdateType, AbstractHandlerAction handlerAction) : base(handlingUpdateType) + { + HandlerAction = handlerAction; + } + + /// + /// Executes the wrapped handler action. + /// + /// The handler container with execution context. + /// The cancellation token. + /// A task representing the asynchronous execution. + public override Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + => HandlerAction.Invoke(container, cancellation); + } +} diff --git a/Telegrator/Handlers/Building/Components/HandlerBuilderBase.cs b/Telegrator/Handlers/Building/Components/HandlerBuilderBase.cs new file mode 100644 index 0000000..a592047 --- /dev/null +++ b/Telegrator/Handlers/Building/Components/HandlerBuilderBase.cs @@ -0,0 +1,202 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Annotations.StateKeeping; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Handlers.Building.Components +{ + /// + /// Base class for building handler descriptors and managing handler filters. + /// + public abstract class HandlerBuilderBase(Type buildingHandlerType, UpdateType updateType, IHandlersCollection? handlerCollection) : IHandlerBuilder + { + private static int HandlerServiceKeyIndex = 0; + + /// + /// to ehich new builded handlers is adding + /// + protected readonly IHandlersCollection? HandlerCollection = handlerCollection; + + /// + /// of building handler + /// + protected readonly UpdateType UpdateType = updateType; + + /// + /// Type of handler to build + /// + protected readonly Type BuildingHandlerType = buildingHandlerType; + + /// + /// Filters applied to handler + /// + protected readonly List> Filters = []; + + /// + /// of building handler + /// + protected DescriptorIndexer Indexer = new DescriptorIndexer(0, 0, 0); + + /// + /// Update validation filter of building handler + /// + protected IFilter? ValidateFilter; + + /// + /// State keeper of building handler + /// + protected IFilter? StateKeeper; + + /// + /// Builds an implicit for the specified handler instance. + /// + /// The instance. + /// The created . + protected HandlerDescriptor BuildImplicitDescriptor(UpdateHandlerBase instance) + { + object handlerServiceKey = GetImplicitHandlerServiceKey(BuildingHandlerType); + + HandlerDescriptor descriptor = new HandlerDescriptor( + DescriptorType.Implicit, BuildingHandlerType, + UpdateType, Indexer, ValidateFilter, + Filters.ToArray(), StateKeeper, + handlerServiceKey, instance); + + HandlerCollection?.AddDescriptor(descriptor); + return descriptor; + } + + /// + /// Gets a unique service key for an implicit handler type. + /// + /// The handler type. + /// A unique service key string. + public static object GetImplicitHandlerServiceKey(Type BuildingHandlerType) + => string.Format("ImplicitHandler_{0}+{1}", HandlerServiceKeyIndex++, BuildingHandlerType.Name); + + /// + /// Sets the update validating action for the handler. + /// + /// The to use. + /// The builder instance. + public void SetUpdateValidating(UpdateValidateAction validateAction) + { + ValidateFilter = new UpdateValidateFilter(validateAction); + } + + /// + /// Sets the concurrency level for the handler. + /// + /// The concurrency value. + /// The builder instance. + public void SetConcurreny(int concurrency) + { + Indexer = Indexer.UpdateConcurrency(concurrency); + } + + /// + /// Sets the priority for the handler. + /// + /// The priority value. + /// The builder instance. + public void SetPriority(int priority) + { + Indexer = Indexer.UpdatePriority(priority); + } + + /// + /// Sets both concurrency and priority for the handler. + /// + /// The concurrency value. + /// The priority value. + /// The builder instance. + public void SetIndexer(int concurrency, int priority) + { + Indexer = new DescriptorIndexer(0, concurrency, priority); + } + + /// + /// Adds a filter to the handler. + /// + /// The to add. + /// The builder instance. + public void AddFilter(IFilter filter) + { + Filters.Add(filter); + } + + /// + /// Adds multiple filters to the handler. + /// + /// The filters to add. + /// The builder instance. + public void AddFilters(params IFilter[] filters) + { + Filters.AddRange(filters); + } + + /// + /// Sets a state keeper for the handler using a specific state and key resolver. + /// + /// The type of the key. + /// The type of the state. + /// The type of the state keeper. + /// The state value. + /// The key resolver. + /// The builder instance. + public void SetStateKeeper(TState myState, IStateKeyResolver keyResolver) + where TKey : notnull + where TState : IEquatable + where TKeeper : StateKeeperBase, new() + { + StateKeeper = new StateKeepFilter(myState, keyResolver); + } + + /// + /// Sets a state keeper for the handler using a special state and key resolver. + /// + /// The type of the key. + /// The type of the state. + /// The type of the state keeper. + /// The special state value. + /// The key resolver. + /// The builder instance. + public void SetStateKeeper(SpecialState specialState, IStateKeyResolver keyResolver) + where TKey : notnull + where TState : IEquatable + where TKeeper : StateKeeperBase, new() + { + StateKeeper = new StateKeepFilter(specialState, keyResolver); + } + + /// + /// Adds a targeted filter for a specific filter target type. + /// + /// The type of the filter target. + /// Function to get the filter target from an update. + /// The filter to add. + /// The builder instance. + public void AddTargetedFilter(Func getFilterringTarget, IFilter filter) where TFilterTarget : class + { + AnonymousTypeFilter anonymousTypeFilter = AnonymousTypeFilter.Compile(filter, getFilterringTarget); + Filters.Add(anonymousTypeFilter); + } + + /// + /// Adds multiple targeted filters for a specific filter target type. + /// + /// The type of the filter target. + /// Function to get the filter target from an update. + /// The filters to add. + /// The builder instance. + public void AddTargetedFilters(Func getFilterringTarget, params IFilter[] filters) where TFilterTarget : class + { + AnonymousCompiledFilter compiledPollingFilter = AnonymousCompiledFilter.Compile(filters, getFilterringTarget); + Filters.Add(compiledPollingFilter); + } + } +} diff --git a/Telegrator/Handlers/Building/Components/IAwaiterHandlerBuilder.cs b/Telegrator/Handlers/Building/Components/IAwaiterHandlerBuilder.cs new file mode 100644 index 0000000..a2bba04 --- /dev/null +++ b/Telegrator/Handlers/Building/Components/IAwaiterHandlerBuilder.cs @@ -0,0 +1,19 @@ +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Handlers.Building.Components +{ + /// + /// Defines a builder for awaiting handler logic for a specific update type. + /// + /// The type of update to await. + public interface IAwaiterHandlerBuilder : IHandlerBuilder where TUpdate : class + { + /// + /// Awaits an update using the specified key resolver and cancellation token. + /// + /// The to resolve the key. + /// The cancellation token. + /// A representing the awaited update. + public Task Await(IStateKeyResolver keyResolver, CancellationToken cancellationToken = default); + } +} diff --git a/Telegrator/Handlers/Building/Components/IHandlerBuilder.cs b/Telegrator/Handlers/Building/Components/IHandlerBuilder.cs new file mode 100644 index 0000000..f280f4d --- /dev/null +++ b/Telegrator/Handlers/Building/Components/IHandlerBuilder.cs @@ -0,0 +1,104 @@ +using Telegram.Bot.Types; +using Telegrator.Annotations.StateKeeping; +using Telegrator.Filters.Components; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Handlers.Building.Components +{ + /// + /// Defines builder actions for configuring handler builders. + /// + public interface IHandlerBuilder + { + /// + /// Sets the update validating action for the handler. + /// + /// The to use. + /// The builder instance. + public void SetUpdateValidating(UpdateValidateAction validateAction); + + /// + /// Sets the concurrency level for the handler. + /// + /// The concurrency value. + /// The builder instance. + public void SetConcurreny(int concurrency); + + /// + /// Sets the priority for the handler. + /// + /// The priority value. + /// The builder instance. + public void SetPriority(int priority); + + /// + /// Sets both concurrency and priority for the handler. + /// + /// The concurrency value. + /// The priority value. + /// The builder instance. + public void SetIndexer(int concurrency, int priority); + + /// + /// Adds a filter to the handler. + /// + /// The to add. + /// The builder instance. + public void AddFilter(IFilter filter); + + /// + /// Adds multiple filters to the handler. + /// + /// The filters to add. + /// The builder instance. + public void AddFilters(params IFilter[] filters); + + /// + /// Sets a state keeper for the handler using a specific state and key resolver. + /// + /// The type of the key. + /// The type of the state. + /// The type of the state keeper. + /// The state value. + /// The key resolver. + /// The builder instance. + public void SetStateKeeper(TState myState, IStateKeyResolver keyResolver) + where TKey : notnull + where TState : IEquatable + where TKeeper : StateKeeperBase, new(); + + /// + /// Sets a state keeper for the handler using a special state and key resolver. + /// + /// The type of the key. + /// The type of the state. + /// The type of the state keeper. + /// The special state value. + /// The key resolver. + /// The builder instance. + public void SetStateKeeper(SpecialState specialState, IStateKeyResolver keyResolver) + where TKey : notnull + where TState : IEquatable + where TKeeper : StateKeeperBase, new(); + + /// + /// Adds a targeted filter for a specific filter target type. + /// + /// The type of the filter target. + /// Function to get the filter target from an update. + /// The filter to add. + /// The builder instance. + public void AddTargetedFilter(Func getFilterringTarget, IFilter filter) + where TFilterTarget : class; + + /// + /// Adds multiple targeted filters for a specific filter target type. + /// + /// The type of the filter target. + /// Function to get the filter target from an update. + /// The filters to add. + /// The builder instance. + public void AddTargetedFilters(Func getFilterringTarget, params IFilter[] filters) + where TFilterTarget : class; + } +} diff --git a/Telegrator/Handlers/Building/Components/IRegularHandlerBuilder.cs b/Telegrator/Handlers/Building/Components/IRegularHandlerBuilder.cs new file mode 100644 index 0000000..10e7065 --- /dev/null +++ b/Telegrator/Handlers/Building/Components/IRegularHandlerBuilder.cs @@ -0,0 +1,18 @@ +using Telegrator.Handlers.Building; +using Telegrator.MadiatorCore; + +namespace Telegrator.Handlers.Building.Components +{ + /// + /// Defines a builder for regular handler logic for a specific update type. + /// + /// The type of update to handle. + public interface IRegularHandlerBuilder : IHandlerBuilder where TUpdate : class + { + /// + /// Builds the handler logic using the specified execution delegate. + /// + /// The delegate to execute the handler logic. + public IHandlersCollection Build(AbstractHandlerAction executeHandler); + } +} diff --git a/Telegrator/Handlers/Building/Components/StateKeepFilter.cs b/Telegrator/Handlers/Building/Components/StateKeepFilter.cs new file mode 100644 index 0000000..41534e4 --- /dev/null +++ b/Telegrator/Handlers/Building/Components/StateKeepFilter.cs @@ -0,0 +1,80 @@ +using Telegram.Bot.Types; +using Telegrator.Annotations.StateKeeping; +using Telegrator.Filters; +using Telegrator.Filters.Components; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.Handlers.Building.Components +{ + /// + /// Filter for state keeping logic, allowing filtering based on state and special state conditions. + /// + /// The type of the key for state resolution. + /// The type of the state. + /// The type of the state keeper. + public class StateKeepFilter : Filter + where TKey : notnull + where TState : IEquatable + where TKeeper : StateKeeperBase, new() + { + /// + /// Gets or sets the state keeper instance. + /// + public static TKeeper StateKeeper { get; internal set; } = null!; + + /// + /// Gets the state value for this filter. + /// + public TState MyState { get; private set; } + + /// + /// Gets the special state value for this filter. + /// + public SpecialState SpecialState { get; private set; } + + /// + /// Initializes a new instance of the class with a specific state. + /// + /// The state value. + /// The key resolver. + public StateKeepFilter(TState myState, IStateKeyResolver keyResolver) + { + StateKeeper ??= new TKeeper(); + StateKeeper.KeyResolver = keyResolver; + MyState = myState; + SpecialState = SpecialState.None; + } + + /// + /// Initializes a new instance of the class with a special state. + /// + /// The special state value. + /// The key resolver. + public StateKeepFilter(SpecialState specialState, IStateKeyResolver keyResolver) + { + StateKeeper ??= new TKeeper(); + StateKeeper.KeyResolver = keyResolver; + MyState = StateKeeper.DefaultState; + SpecialState = specialState; + } + + /// + /// Determines whether the filter can pass for the given context based on state logic. + /// + /// The filter execution context. + /// True if the filter passes; otherwise, false. + public override bool CanPass(FilterExecutionContext 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); + } + } +} diff --git a/Telegrator/Handlers/Building/Components/UpdateValidateFilter.cs b/Telegrator/Handlers/Building/Components/UpdateValidateFilter.cs new file mode 100644 index 0000000..c721ecb --- /dev/null +++ b/Telegrator/Handlers/Building/Components/UpdateValidateFilter.cs @@ -0,0 +1,41 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.Handlers.Building.Components +{ + /// + /// Delegate for validating an update in a filter context. + /// + /// The filter execution context. + /// True if the update is valid; otherwise, false. + public delegate bool UpdateValidateAction(FilterExecutionContext context); + + /// + /// Filter that uses a delegate to validate updates. + /// + public class UpdateValidateFilter : IFilter + { + /// + /// Gets a value indicating whether this filter is collectable. Always false for this filter. + /// + public bool IsCollectible => false; + private readonly UpdateValidateAction UpdateValidateAction; + + /// + /// Initializes a new instance of the class. + /// + /// The validation delegate to use. + public UpdateValidateFilter(UpdateValidateAction updateValidateAction) + { + UpdateValidateAction = updateValidateAction; + } + + /// + /// Determines whether the filter can pass for the given context using the validation delegate. + /// + /// The filter execution context. + /// True if the filter passes; otherwise, false. + public bool CanPass(FilterExecutionContext info) + => UpdateValidateAction.Invoke(info); + } +} diff --git a/Telegrator/Handlers/Building/HandlerBuilder.cs b/Telegrator/Handlers/Building/HandlerBuilder.cs new file mode 100644 index 0000000..c7cd88b --- /dev/null +++ b/Telegrator/Handlers/Building/HandlerBuilder.cs @@ -0,0 +1,51 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Providers; +using Telegrator.Handlers.Building.Components; +using Telegrator.MadiatorCore; + +namespace Telegrator.Handlers.Building +{ + /// + /// Delegate for handler execution actions that take a container and cancellation token. + /// + /// The type of update being handled. + /// The handler container with execution context. + /// The cancellation token. + /// A task representing the asynchronous execution. + public delegate Task AbstractHandlerAction(IAbstractHandlerContainer container, CancellationToken cancellation) where TUpdate : class; + + /// + /// Builder class for creating regular handlers that can process updates. + /// Provides fluent API for configuring filters, state keepers, and other handler properties. + /// + /// The type of update to handle. + public class HandlerBuilder : HandlerBuilderBase, IRegularHandlerBuilder where TUpdate : class + { + /// + /// Initializes a new instance of the class. + /// + /// The type of update this handler will process. + /// The collection to register the built handler with. + /// Thrown when the update type is not valid for TUpdate. + public HandlerBuilder(UpdateType updateType, IHandlersCollection handlerCollection) : base(typeof(BuildedAbstractHandler), updateType, handlerCollection) + { + if (!updateType.IsValidUpdateObject()) + throw new ArgumentException("\"UpdateType." + updateType + "\" is not valid type for \"" + nameof(TUpdate) + "\" update object", nameof(updateType)); + } + + /// + /// Builds an abstract handler with the specified execution action. + /// + /// The delegate action to execute when the handler is invoked. + /// Thrown when executeHandler is null. + public IHandlersCollection Build(AbstractHandlerAction executeHandler) + { + if (executeHandler == null) + throw new ArgumentNullException(nameof(executeHandler)); + + BuildedAbstractHandler instance = new BuildedAbstractHandler(UpdateType, executeHandler); + BuildImplicitDescriptor(instance); + return HandlerCollection!; + } + } +} diff --git a/Telegrator/Handlers/CallbackQueryHandler.cs b/Telegrator/Handlers/CallbackQueryHandler.cs new file mode 100644 index 0000000..5a415e4 --- /dev/null +++ b/Telegrator/Handlers/CallbackQueryHandler.cs @@ -0,0 +1,44 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Attributes; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; + +namespace Telegrator.Handlers +{ + /// + /// Attribute that marks a handler to process callback query updates. + /// This handler will be triggered when users interact with inline keyboards or other callback mechanisms. + /// + /// The maximum number of concurrent executions allowed (default: 0 for unlimited). + public sealed class CallbackQueryHandlerAttribute(int concurrency = 0) : UpdateHandlerAttribute(UpdateType.CallbackQuery, concurrency) + { + /// + /// Always returns true, allowing any callback query update to pass through this filter. + /// + /// The filter execution context (unused). + /// Always returns true to allow any callback query update. + public override bool CanPass(FilterExecutionContext context) => true; + } + + /// + /// Abstract base class for handlers that process callback query updates. + /// Provides a foundation for creating handlers that respond to user interactions with inline keyboards. + /// + public abstract class CallbackQueryHandler() : AbstractUpdateHandler(UpdateType.CallbackQuery) + { + /// + /// Gets the type-specific data from the callback query. + /// Returns the data string, chat instance, or game short name depending on the callback query type. + /// + protected string TypeData + { + get => Input switch + { + { Data: { } data } => data, + { ChatInstance: { } chatInstance } => chatInstance, + { GameShortName: { } gameShortName } => gameShortName + }; + } + } +} diff --git a/Telegrator/Handlers/CommandHandler.cs b/Telegrator/Handlers/CommandHandler.cs new file mode 100644 index 0000000..d70ca8d --- /dev/null +++ b/Telegrator/Handlers/CommandHandler.cs @@ -0,0 +1,174 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Attributes; +using Telegrator.Filters.Components; + +namespace Telegrator.Handlers +{ + /// + /// Attribute that marks a handler to process command messages. + /// This handler will be triggered when users send bot commands (messages starting with '/'). + /// + /// The maximum number of concurrent executions allowed (default: 1). + public class CommandHandlerAttribute(int concurrency = 1) : UpdateHandlerAttribute(UpdateType.Message, concurrency) + { + /// + /// Gets the command that was extracted from the message (without the '/' prefix and bot username). + /// + public string ReceivedCommand { get; private set; } = null!; + + /// + /// Checks if the update contains a valid bot command and extracts the command text. + /// + /// The filter execution context containing the update. + /// True if the update contains a valid bot command; otherwise, false. + public override bool CanPass(FilterExecutionContext context) + { + if (context.Input.Message is not { Entities.Length: > 0, Text.Length: > 0 } message) + return false; + + MessageEntity commandEntity = message.Entities[0]; + if (commandEntity.Type != MessageEntityType.BotCommand) + return false; + + ReceivedCommand = message.Text.Substring(commandEntity.Offset + 1, commandEntity.Length - 1); + if (ReceivedCommand.Contains('@')) + { + string[] split = ReceivedCommand.Split('@'); + ReceivedCommand = split[0]; + } + + return true; + } + } + + /// + /// Abstract base class for handlers that process command messages. + /// Provides functionality to extract and parse command arguments. + /// + public abstract class CommandHandler : MessageHandler + { + /// + /// Cached array of command arguments. + /// + private string[]? _cmdArgsSplit; + + /// + /// Cached string representation of command arguments. + /// + private string? _argsString; + + /// + /// Gets the command that was extracted from the message. + /// + protected string ReceivedCommand + { + get => CompletedFilters.Get(0).ReceivedCommand; + } + + /// + /// Gets the arguments string (everything after the command). + /// + protected string ArgumentsString + { + get => _argsString ??= ArgsStringify(); + } + + /// + /// Gets the command arguments as an array of strings. + /// + protected string[] Arguments + { + get => _cmdArgsSplit ??= SplitArgs(); + } + + /// + /// Splits the command arguments into an array of strings. + /// + /// An array of command arguments. + private string[] SplitArgs() + { + if (Input.Text is not { Length: > 0 }) + return []; + + return Input.Text.Split([" "], StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray(); + } + + /// + /// Extracts the arguments string from the command message. + /// + /// The arguments string (everything after the command). + private string ArgsStringify() + { + if (Input.Text is not { Length: > 0 }) + return string.Empty; + + return Input.Text.Substring(ReceivedCommand.Length + 1); + } + } + + /// + /// Abstract base class for branching handlers that process command messages. + /// Provides functionality to extract and parse command arguments for branching scenarios. + /// + public abstract class BranchingCommandHandler : BranchingMessageHandler + { + /// + /// Cached array of command arguments. + /// + private string[]? _cmdArgsSplit; + + /// + /// Cached string representation of command arguments. + /// + private string? _argsString; + + /// + /// Gets the command that was extracted from the message. + /// + protected string ReceivedCommand + { + get => CompletedFilters.Get(0).ReceivedCommand; + } + + /// + /// Gets the arguments string (everything after the command). + /// + protected string ArgumentsString + { + get => _argsString ??= ArgsStringify(); + } + + /// + /// Gets the command arguments as an array of strings. + /// + protected string[] Arguments + { + get => _cmdArgsSplit ??= SplitArgs(); + } + + /// + /// Splits the command arguments into an array of strings. + /// + /// An array of command arguments. + private string[] SplitArgs() + { + if (Input.Text is not { Length: > 0 }) + return []; + + return Input.Text.Split([" "], StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray(); + } + + /// + /// Extracts the arguments string from the command message. + /// + /// The arguments string (everything after the command). + private string ArgsStringify() + { + if (Input.Text is not { Length: > 0 }) + return string.Empty; + + return Input.Text.Substring(ReceivedCommand.Length + 1); + } + } +} diff --git a/Telegrator/Handlers/Components/AbstractUpdateHandler.cs b/Telegrator/Handlers/Components/AbstractUpdateHandler.cs new file mode 100644 index 0000000..db5b9c4 --- /dev/null +++ b/Telegrator/Handlers/Components/AbstractUpdateHandler.cs @@ -0,0 +1,91 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Filters.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Handlers.Components +{ + /// + /// Abstract handler for Telegram updates of type . + /// + public abstract class AbstractUpdateHandler : UpdateHandlerBase, IHandlerContainerFactory where TUpdate : class + { + /// + /// Handler container for the current update. + /// + protected IAbstractHandlerContainer Container { get; private set; } = default!; + + /// + /// Telegram Bot client associated with the current container. + /// + protected ITelegramBotClient Client => Container.Client; + + /// + /// Incoming update of type . + /// + protected TUpdate Input => Container.ActualUpdate; + + /// + /// The Telegram update being handled. + /// + protected Update HandlingUpdate => Container.HandlingUpdate; + + /// + /// Additional data associated with the handler execution. + /// + protected Dictionary ExtraData => Container.ExtraData; + + /// + /// List of successfully passed filters. + /// + protected CompletedFiltersList CompletedFilters => Container.CompletedFilters; + + /// + /// Provider for awaiting asynchronous operations. + /// + protected IAwaitingProvider AwaitingProvider => Container.AwaitingProvider; + + /// + /// Initializes a new instance and checks that the update type matches . + /// + /// The type of update to handle. + protected AbstractUpdateHandler(UpdateType handlingUpdateType) : base(handlingUpdateType) + { + if (!HandlingUpdateType.IsValidUpdateObject()) + throw new Exception(); + } + + /// + /// Creates a handler container for the specified awaiting provider and handler info. + /// + /// The awaiting provider. + /// The handler descriptor info. + /// The created handler container. + public virtual IHandlerContainer CreateContainer(IAwaitingProvider awaitingProvider, DescribedHandlerInfo handlerInfo) + { + return new AbstractHandlerContainer(awaitingProvider, handlerInfo); + } + + /// + /// Executes the handler logic using the specified container. + /// + /// The handler container. + /// Cancellation token. + /// A task representing the asynchronous operation. + protected override sealed async Task ExecuteInternal(IHandlerContainer container, CancellationToken cancellationToken) + { + Container = (IAbstractHandlerContainer)container; + await Execute(Container, cancellationToken); + } + + /// + /// Abstract method to execute the update handling logic. + /// + /// The handler container. + /// Cancellation token. + /// A task representing the asynchronous operation. + public abstract Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation); + } +} diff --git a/Telegrator/Handlers/Components/BranchingUpdateHandler.cs b/Telegrator/Handlers/Components/BranchingUpdateHandler.cs new file mode 100644 index 0000000..670bdfe --- /dev/null +++ b/Telegrator/Handlers/Components/BranchingUpdateHandler.cs @@ -0,0 +1,167 @@ +using System.Reflection; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Attributes.Components; +using Telegrator.Filters.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; +using Telegrator.Providers; + +namespace Telegrator.Handlers.Components +{ + /// + /// Abstract base class for handlers that support branching execution based on different methods. + /// Allows multiple handler methods to be defined in a single class, each with its own filters. + /// + /// The type of update being handled. + public abstract class BranchingUpdateHandler : AbstractUpdateHandler, IHandlerContainerFactory, ICustomDescriptorsProvider where TUpdate : class + { + /// + /// The method info for the current branch being executed. + /// + private MethodInfo? branchMethodInfo = null; + + /// + /// Gets the binding flags used to discover branch methods. + /// + protected virtual BindingFlags BranchesBindingFlags => BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public; + + /// + /// Gets the allowed return types for branch methods. + /// + protected virtual Type[] AllowedBranchReturnTypes => [typeof(void), typeof(Task)]; + + /// + /// Gets the cancellation token for the current execution. + /// + protected CancellationToken Cancellation { get; private set; } = default; + + /// + /// Initializes a new instance of the class. + /// + /// The type of update this handler processes. + protected BranchingUpdateHandler(UpdateType handlingUpdateType) + : base(handlingUpdateType) { } + + /// + /// Initializes a new instance of the class with a specific branch method. + /// + /// The type of update this handler processes. + /// The specific branch method to execute. + protected BranchingUpdateHandler(UpdateType handlingUpdateType, MethodInfo branch) + : base(handlingUpdateType) => branchMethodInfo = branch; + + /// + /// Describes all handler branches in this class. + /// + /// A collection of handler descriptors for each branch method. + /// Thrown when no branch methods are found. + public IEnumerable DescribeHandlers() + { + Type thisType = GetType(); + UpdateHandlerAttributeBase updateHandlerAttribute = HandlerInspector.GetHandlerAttribute(thisType); + IEnumerable> handlerFilters = HandlerInspector.GetFilterAttributes(thisType, HandlingUpdateType); + + MethodInfo[] handlerBranches = thisType.GetMethods().Where(branch => branch.DeclaringType == thisType).ToArray(); + if (handlerBranches.Length == 0) + throw new Exception(); + + foreach (MethodInfo branch in handlerBranches) + yield return DescribeBranch(branch, updateHandlerAttribute, handlerFilters); + } + + /// + /// Describes a specific branch method. + /// + /// The branch method to describe. + /// The handler attribute for the class. + /// The filters applied to the class. + /// A handler descriptor for the branch method. + /// Thrown when the branch method has parameters or invalid return type. + protected virtual HandlerDescriptor DescribeBranch(MethodInfo branch, UpdateHandlerAttributeBase handlerAttribute, IEnumerable> handlerFilters) + { + Type thisType = GetType(); + + if (branch.GetParameters().Length != 0) + throw new Exception(); + + if (!AllowedBranchReturnTypes.Any(branch.ReturnType.Equals)) + throw new Exception(); + + List> branchFiltersList = HandlerInspector.GetFilterAttributes(branch, HandlingUpdateType).ToList(); + branchFiltersList.AddRange(handlerFilters); + + DescriptorFiltersSet filtersSet = new DescriptorFiltersSet( + handlerAttribute, + HandlerInspector.GetStateKeeperAttribute(branch), + branchFiltersList.ToArray()); + + return new HandlerBranchDescriptor(branch, HandlingUpdateType, handlerAttribute.GetIndexer(), filtersSet); + } + + /// + /// Creates a handler container for this branching handler. + /// + /// The awaiting provider for the container. + /// The handler information. + /// A handler container for this branching handler. + /// Thrown when the awaiting provider is not of the expected type. + public override IHandlerContainer CreateContainer(IAwaitingProvider awaitingProvider, DescribedHandlerInfo handlerInfo) + { + return new AbstractHandlerContainer(awaitingProvider, handlerInfo); + } + + /// + /// Executes the current branch method. + /// + /// The handler container. + /// The cancellation token. + /// Thrown when no branch method is set. + public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + if (branchMethodInfo is null) + throw new Exception(); + + Cancellation = cancellation; + await BranchExecuteWrapper(container, branchMethodInfo); + } + + /// + /// Wraps the execution of a branch method, handling both void and Task return types. + /// + /// The handler container. + /// The method to execute. + protected virtual async Task BranchExecuteWrapper(IAbstractHandlerContainer container, MethodInfo methodInfo) + { + if (methodInfo.ReturnType == typeof(void)) + { + methodInfo.Invoke(this, []); + return; + } + else + { + object branchReturn = methodInfo.Invoke(this, []); + if (branchReturn == null) + return; + + if (branchReturn is Task branchTask) + await branchTask; + } + } + + private class HandlerBranchDescriptor : HandlerDescriptor + { + public HandlerBranchDescriptor(MethodInfo method, UpdateType updateType, DescriptorIndexer indexer, DescriptorFiltersSet filters) + : base(DescriptorType.General, method.DeclaringType, updateType, indexer, filters) + { + DisplayString = string.Format("{0}+{1}", method.DeclaringType.Name, method.Name); + InstanceFactory = () => + { + BranchingUpdateHandler handler = (BranchingUpdateHandler)Activator.CreateInstance(method.DeclaringType); + handler.branchMethodInfo = method; + return handler; + }; + } + } + } +} diff --git a/Telegrator/Handlers/Components/EmptyHandlerContainer.cs b/Telegrator/Handlers/Components/EmptyHandlerContainer.cs new file mode 100644 index 0000000..61e8874 --- /dev/null +++ b/Telegrator/Handlers/Components/EmptyHandlerContainer.cs @@ -0,0 +1,28 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegrator.Filters.Components; +using Telegrator.MadiatorCore; + +namespace Telegrator.Handlers.Components +{ + /// + /// Represents an empty handler container that throws for all members. + /// + public class EmptyHandlerContainer : IHandlerContainer + { + /// + public Update HandlingUpdate => throw new NotImplementedException(); + + /// + public ITelegramBotClient Client => throw new NotImplementedException(); + + /// + public Dictionary ExtraData => throw new NotImplementedException(); + + /// + public CompletedFiltersList CompletedFilters => throw new NotImplementedException(); + + /// + public IAwaitingProvider AwaitingProvider => throw new NotImplementedException(); + } +} diff --git a/Telegrator/Handlers/Components/IHandlerContainer.cs b/Telegrator/Handlers/Components/IHandlerContainer.cs new file mode 100644 index 0000000..9948b34 --- /dev/null +++ b/Telegrator/Handlers/Components/IHandlerContainer.cs @@ -0,0 +1,39 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegrator.Filters.Components; +using Telegrator.MadiatorCore; + +namespace Telegrator.Handlers.Components +{ + /// + /// Interface for handler containers that provide context and resources for update handlers. + /// Contains all necessary information and services that handlers need during execution. + /// + public interface IHandlerContainer + { + /// + /// Gets the being handled. + /// + public Update HandlingUpdate { get; } + + /// + /// Gets the used for this handler. + /// + public ITelegramBotClient Client { get; } + + /// + /// Gets the extra data associated with the handler execution. + /// + public Dictionary ExtraData { get; } + + /// + /// Gets the for this handler. + /// + public CompletedFiltersList CompletedFilters { get; } + + /// + /// Gets the for awaiting operations. + /// + public IAwaitingProvider AwaitingProvider { get; } + } +} diff --git a/Telegrator/Handlers/Components/IHandlerContainerFactory.cs b/Telegrator/Handlers/Components/IHandlerContainerFactory.cs new file mode 100644 index 0000000..b2ce3c4 --- /dev/null +++ b/Telegrator/Handlers/Components/IHandlerContainerFactory.cs @@ -0,0 +1,20 @@ +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Handlers.Components +{ + /// + /// Factory interface for creating handler containers. + /// Provides a way to create handler containers with specific providers and handler information. + /// + public interface IHandlerContainerFactory + { + /// + /// Creates a new for the specified awaiting provider and handler info. + /// + /// The to use. + /// The for the handler. + /// A new instance. + public IHandlerContainer CreateContainer(IAwaitingProvider awaitingProvider, DescribedHandlerInfo handlerInfo); + } +} diff --git a/Telegrator/Handlers/Components/UpdateHandlerBase.cs b/Telegrator/Handlers/Components/UpdateHandlerBase.cs new file mode 100644 index 0000000..a4ef62f --- /dev/null +++ b/Telegrator/Handlers/Components/UpdateHandlerBase.cs @@ -0,0 +1,41 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Handlers.Components +{ + /// + /// Base class for update handlers, providing execution and lifetime management for Telegram updates. + /// + public abstract class UpdateHandlerBase(UpdateType handlingUpdateType) + { + /// + /// Gets the that this handler processes. + /// + public UpdateType HandlingUpdateType { get; } = handlingUpdateType; + + /// + /// Gets the associated with this handler instance. + /// + public HandlerLifetimeToken LifetimeToken { get; } = new HandlerLifetimeToken(); + + /// + /// Executes the handler logic and marks the lifetime as ended after execution. + /// + /// The for the update. + /// The cancellation token. + /// A representing the asynchronous operation. + public async Task Execute(IHandlerContainer container, CancellationToken cancellationToken = default) + { + await ExecuteInternal(container, cancellationToken); + LifetimeToken.LifetimeEnded(); + } + + /// + /// Executes the handler logic for the given container and cancellation token. + /// + /// The for the update. + /// The cancellation token. + /// A representing the asynchronous operation. + protected abstract Task ExecuteInternal(IHandlerContainer container, CancellationToken cancellationToken); + } +} diff --git a/Telegrator/Handlers/IAbstractHandlerContainer.cs b/Telegrator/Handlers/IAbstractHandlerContainer.cs new file mode 100644 index 0000000..0cd91c6 --- /dev/null +++ b/Telegrator/Handlers/IAbstractHandlerContainer.cs @@ -0,0 +1,16 @@ +using Telegrator.Handlers.Components; + +namespace Telegrator.Handlers +{ + /// + /// Represents a handler container for a specific update type. + /// + /// The type of update handled by the container. + public interface IAbstractHandlerContainer : IHandlerContainer where TUpdate : class + { + /// + /// Gets the actual update object of type . + /// + public TUpdate ActualUpdate { get; } + } +} diff --git a/Telegrator/Handlers/MessageHandler.cs b/Telegrator/Handlers/MessageHandler.cs new file mode 100644 index 0000000..b651db9 --- /dev/null +++ b/Telegrator/Handlers/MessageHandler.cs @@ -0,0 +1,191 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.ReplyMarkups; +using Telegrator.Attributes; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; + +namespace Telegrator.Handlers +{ + /// + /// Attribute that marks a handler to process message updates. + /// This handler will be triggered when users send messages in chats. + /// + /// The maximum number of concurrent executions allowed (default: 0 for unlimited). + public class MessageHandlerAttribute(int concurrency = 0) : UpdateHandlerAttribute(UpdateType.Message, concurrency) + { + /// + /// Checks if the update contains a valid message. + /// + /// The filter execution context containing the update. + /// True if the update contains a message; otherwise, false. + public override bool CanPass(FilterExecutionContext context) => context.Input is { Message: { } }; + } + + /// + /// Abstract base class for handlers that process message updates. + /// Provides convenient methods for sending replies and responses to messages. + /// + public abstract class MessageHandler() : AbstractUpdateHandler(UpdateType.Message) + { + /// + /// Sends a reply message to the current message. + /// + /// The text of the message to send. + /// The parse mode for the message text. + /// The reply markup for the message. + /// Options for link preview generation. + /// The thread ID for forum topics. + /// The message entities to include. + /// Whether to disable notification for the message. + /// Whether to protect the message content. + /// The message effect ID. + /// The business connection ID. + /// Whether to allow paid broadcast. + /// The cancellation token. + /// The sent message. + protected async Task Reply( + string text, + ParseMode parseMode = ParseMode.None, + ReplyMarkup? replyMarkup = null, + LinkPreviewOptions? linkPreviewOptions = null, + int? messageThreadId = null, + IEnumerable? entities = null, + bool disableNotification = false, + bool protectContent = false, + string? messageEffectId = null, + string? businessConnectionId = null, + bool allowPaidBroadcast = false, + CancellationToken cancellationToken = default) + => await Client.SendMessage( + Input.Chat, text, parseMode, Input, + replyMarkup, linkPreviewOptions, + messageThreadId, entities, + disableNotification, protectContent, + messageEffectId, businessConnectionId, + allowPaidBroadcast, cancellationToken); + + /// + /// Sends a response message to the current chat. + /// + /// The text of the message to send. + /// The parse mode for the message text. + /// The reply parameters for the message. + /// The reply markup for the message. + /// Options for link preview generation. + /// The thread ID for forum topics. + /// The message entities to include. + /// Whether to disable notification for the message. + /// Whether to protect the message content. + /// The message effect ID. + /// The business connection ID. + /// Whether to allow paid broadcast. + /// The cancellation token. + /// The sent message. + protected async Task Responce( + string text, + ParseMode parseMode = ParseMode.None, + ReplyParameters? replyParameters = null, + ReplyMarkup? replyMarkup = null, + LinkPreviewOptions? linkPreviewOptions = null, + int? messageThreadId = null, + IEnumerable? entities = null, + bool disableNotification = false, + bool protectContent = false, + string? messageEffectId = null, + string? businessConnectionId = null, + bool allowPaidBroadcast = false, + CancellationToken cancellationToken = default) + => await Client.SendMessage( + Input.Chat, text, parseMode, replyParameters, + replyMarkup, linkPreviewOptions, + messageThreadId, entities, + disableNotification, protectContent, + messageEffectId, businessConnectionId, + allowPaidBroadcast, cancellationToken); + } + + /// + /// Abstract base class for branching handlers that process message updates. + /// Provides convenient methods for sending replies and responses to messages in branching scenarios. + /// + public abstract class BranchingMessageHandler() : BranchingUpdateHandler(UpdateType.Message) + { + /// + /// Sends a reply message to the current message. + /// + /// The text of the message to send. + /// The parse mode for the message text. + /// The reply markup for the message. + /// Options for link preview generation. + /// The thread ID for forum topics. + /// The message entities to include. + /// Whether to disable notification for the message. + /// Whether to protect the message content. + /// The message effect ID. + /// The business connection ID. + /// Whether to allow paid broadcast. + /// The cancellation token. + /// The sent message. + protected async Task Reply( + string text, + ParseMode parseMode = ParseMode.None, + ReplyMarkup? replyMarkup = null, + LinkPreviewOptions? linkPreviewOptions = null, + int? messageThreadId = null, + IEnumerable? entities = null, + bool disableNotification = false, + bool protectContent = false, + string? messageEffectId = null, + string? businessConnectionId = null, + bool allowPaidBroadcast = false, + CancellationToken cancellationToken = default) + => await Client.SendMessage( + Input.Chat, text, parseMode, Input, + replyMarkup, linkPreviewOptions, + messageThreadId, entities, + disableNotification, protectContent, + messageEffectId, businessConnectionId, + allowPaidBroadcast, cancellationToken); + + /// + /// Sends a response message to the current chat. + /// + /// The text of the message to send. + /// The parse mode for the message text. + /// The reply parameters for the message. + /// The reply markup for the message. + /// Options for link preview generation. + /// The thread ID for forum topics. + /// The message entities to include. + /// Whether to disable notification for the message. + /// Whether to protect the message content. + /// The message effect ID. + /// The business connection ID. + /// Whether to allow paid broadcast. + /// The cancellation token. + /// The sent message. + protected async Task Responce( + string text, + ParseMode parseMode = ParseMode.None, + ReplyParameters? replyParameters = null, + ReplyMarkup? replyMarkup = null, + LinkPreviewOptions? linkPreviewOptions = null, + int? messageThreadId = null, + IEnumerable? entities = null, + bool disableNotification = false, + bool protectContent = false, + string? messageEffectId = null, + string? businessConnectionId = null, + bool allowPaidBroadcast = false, + CancellationToken cancellationToken = default) + => await Client.SendMessage( + Input.Chat, text, parseMode, replyParameters, + replyMarkup, linkPreviewOptions, + messageThreadId, entities, + disableNotification, protectContent, + messageEffectId, businessConnectionId, + allowPaidBroadcast, cancellationToken); + } +} diff --git a/Telegrator/IReactiveTelegramBot.cs b/Telegrator/IReactiveTelegramBot.cs new file mode 100644 index 0000000..684aea8 --- /dev/null +++ b/Telegrator/IReactiveTelegramBot.cs @@ -0,0 +1,16 @@ +using Telegrator.MadiatorCore; + +namespace Telegrator +{ + /// + /// Interface for reactive Telegram bot implementations. + /// Defines the core properties and capabilities of a reactive bot. + /// + public interface IReactiveTelegramBot + { + /// + /// Gets the update router for handling incoming updates. + /// + public IUpdateRouter UpdateRouter { get; } + } +} diff --git a/Telegrator/MadiatorCore/Descriptors/DefaultCustomDescriptors.cs b/Telegrator/MadiatorCore/Descriptors/DefaultCustomDescriptors.cs new file mode 100644 index 0000000..00c6c0a --- /dev/null +++ b/Telegrator/MadiatorCore/Descriptors/DefaultCustomDescriptors.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using Telegrator.Handlers; +using Telegrator.Handlers.Components; + +namespace Telegrator.MadiatorCore.Descriptors +{ + /* + public class MethodHandlerDescriptor : HandlerDescriptor + { + public MethodHandlerDescriptor() : base(DescriptorType.General, ) + } + + public class MethodHandler : AbstractUpdateHandler + { + public override Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) + { + + } + } + */ +} diff --git a/Telegrator/MadiatorCore/Descriptors/DescribedHandlerInfo.cs b/Telegrator/MadiatorCore/Descriptors/DescribedHandlerInfo.cs new file mode 100644 index 0000000..d98dcfc --- /dev/null +++ b/Telegrator/MadiatorCore/Descriptors/DescribedHandlerInfo.cs @@ -0,0 +1,115 @@ +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore; + +namespace Telegrator.MadiatorCore.Descriptors +{ + /// + /// Contains information about a described handler, including its context, client, and execution logic. + /// + public class DescribedHandlerInfo + { + /// + /// The update router associated with this handler. + /// + public readonly IUpdateRouter UpdateRouter; + + /// + /// The Telegram bot client used for this handler. + /// + public readonly ITelegramBotClient Client; + + /// + /// The handler instance being described. + /// + public readonly UpdateHandlerBase HandlerInstance; + + /// + /// Extra data associated with the handler execution. + /// + public readonly Dictionary ExtraData; + + /// + /// List of completed filters for this handler. + /// + public readonly CompletedFiltersList CompletedFilters; + + /// + /// The update being handled. + /// + public readonly Update HandlingUpdate; + + /// + /// Lifetime token for the handler instance. + /// + public HandlerLifetimeToken HandlerLifetime => HandlerInstance.LifetimeToken; + + /// + /// The handler container created during execution. + /// + public IHandlerContainer? HandlerContainer { get; private set; } + + /// + /// Display string for the handler (for debugging or logging). + /// + public string DisplayString { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The update router. + /// The Telegram bot client. + /// The handler instance. + /// The filter execution context. + /// Optional display string. + public DescribedHandlerInfo(IUpdateRouter updateRouter, ITelegramBotClient client, UpdateHandlerBase handlerInstance, FilterExecutionContext filterContext, string? displayString) + { + UpdateRouter = updateRouter; + Client = client; + HandlerInstance = handlerInstance; + ExtraData = filterContext.Data; + CompletedFilters = filterContext.CompletedFilters; + HandlingUpdate = filterContext.Update; + DisplayString = displayString ?? handlerInstance.GetType().Name; + } + + /// + /// Executes the handler logic asynchronously. + /// + /// Cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the handler lifetime has ended or the handler is not a container factory. + public async Task Execute(CancellationToken cancellationToken) + { + if (HandlerLifetime.IsEnded) + throw new Exception(); + + IHandlerContainerFactory? containerFactory = HandlerInstance is IHandlerContainerFactory handlerDefainedContainerFactory + ? handlerDefainedContainerFactory + : UpdateRouter.DefaultContainerFactory is not null + ? UpdateRouter.DefaultContainerFactory + : throw new Exception(); + + try + { + HandlerContainer = containerFactory.CreateContainer(UpdateRouter.AwaitingProvider, this); + await HandlerInstance.Execute(HandlerContainer, cancellationToken); + } + catch (OperationCanceledException) + { + // Cancelled + _ = 0xBAD + 0xC0DE; + return; + } + catch (Exception exception) + { + await UpdateRouter + .HandleErrorAsync(Client, exception, HandleErrorSource.HandleUpdateError, cancellationToken) + .ConfigureAwait(false); + } + } + } +} diff --git a/Telegrator/MadiatorCore/Descriptors/DescriptorFiltersSet.cs b/Telegrator/MadiatorCore/Descriptors/DescriptorFiltersSet.cs new file mode 100644 index 0000000..f903834 --- /dev/null +++ b/Telegrator/MadiatorCore/Descriptors/DescriptorFiltersSet.cs @@ -0,0 +1,76 @@ +using Telegram.Bot.Types; +using Telegrator.Filters.Components; + +namespace Telegrator.MadiatorCore.Descriptors +{ + /// + /// Represents a set of filters for a handler descriptor, including update and state keeper validators. + /// + public sealed class DescriptorFiltersSet + { + /// + /// Validator for the update object. + /// + public IFilter? UpdateValidator { get; set; } + + /// + /// Validator for the state keeper. + /// + public IFilter? StateKeeperValidator { get; set; } + + /// + /// Array of update filters. + /// + public IFilter[]? UpdateFilters { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// Validator for the update object. + /// Validator for the state keeper. + /// Array of update filters. + public DescriptorFiltersSet(IFilter? updateValidator, IFilter? stateKeeperValidator, IFilter[]? updateFilters) + { + UpdateValidator = updateValidator; + StateKeeperValidator = stateKeeperValidator; + UpdateFilters = updateFilters; + } + + /// + /// Validates the filter context using all filters in the set. + /// + /// The filter execution context. + /// True if all filters pass; otherwise, false. + public bool Validate(FilterExecutionContext filterContext) + { + if (UpdateValidator != null) + { + if (!UpdateValidator.CanPass(filterContext)) + return false; + + filterContext.CompletedFilters.Add(UpdateValidator); + } + + if (StateKeeperValidator != null) + { + if (!StateKeeperValidator.CanPass(filterContext)) + return false; + + filterContext.CompletedFilters.Add(StateKeeperValidator); + } + + if (UpdateFilters != null) + { + foreach (IFilter filter in UpdateFilters) + { + if (!filter.CanPass(filterContext)) + return false; + + filterContext.CompletedFilters.Add(filter); + } + } + + return true; + } + } +} diff --git a/Telegrator/MadiatorCore/Descriptors/DescriptorIndexer.cs b/Telegrator/MadiatorCore/Descriptors/DescriptorIndexer.cs new file mode 100644 index 0000000..5ac4762 --- /dev/null +++ b/Telegrator/MadiatorCore/Descriptors/DescriptorIndexer.cs @@ -0,0 +1,88 @@ +using Telegrator.Attributes.Components; + +namespace Telegrator.MadiatorCore.Descriptors +{ + /// + /// Represents an indexer for handler descriptors, containing concurrency and priority information. + /// + public readonly struct DescriptorIndexer(int routerIndex, int concurrency, int priority) : IComparable + { + /// + /// Index of this descriptor when it was added to router + /// + public readonly int RouterIndex = routerIndex; + + /// + /// Of this handlert type + /// + public readonly int Importance = concurrency; + + /// + /// The priority of the handler. + /// + public readonly int Priority = priority; + + /// + /// Initializes a new instance of the struct from a handler attribute. + /// + /// + /// The handler attribute. + public DescriptorIndexer(int routerIndex, UpdateHandlerAttributeBase pollingHandler) + : this(routerIndex, pollingHandler.Concurrency, pollingHandler.Priority) { } + + /// + /// Returns a new with updated priority. + /// + /// The new priority value. + /// A new instance. + public DescriptorIndexer UpdatePriority(int priority) + => new DescriptorIndexer(RouterIndex, Importance, priority); + + /// + /// Returns a new with updated concurrency. + /// + /// The new concurrency value. + /// A new instance. + public DescriptorIndexer UpdateConcurrency(int concurrency) + => new DescriptorIndexer(RouterIndex, concurrency, Priority); + + /// + /// Returns a new with updated RouterIndex. + /// + /// + /// A new instance. + public DescriptorIndexer UpdateIndex(int routerIndex) + => new DescriptorIndexer(routerIndex, Importance, Priority); + + /// + /// Compares this instance to another . + /// + /// The other indexer to compare to. + /// An integer indicating the relative order. + public int CompareTo(DescriptorIndexer other) + { + int importanceCmp = Importance.CompareTo(other.Importance); + if (importanceCmp != 0) + return importanceCmp; + + int priorityCmp = Priority.CompareTo(other.Priority); + if (priorityCmp != 0) + return priorityCmp; + + int routerIndexCmp = RouterIndex.CompareTo(other.RouterIndex); + if (routerIndexCmp != 0) + return routerIndexCmp; + + return 0; + } + + /// + /// Returns a string representation of the indexer. + /// + /// A string in the format (C:concurrency, P:priority). + public override string ToString() + { + return string.Format("(I:{0}, C:{1}, P:{2})", RouterIndex, Importance, Priority); + } + } +} diff --git a/Telegrator/MadiatorCore/Descriptors/HandlerDescriptor.cs b/Telegrator/MadiatorCore/Descriptors/HandlerDescriptor.cs new file mode 100644 index 0000000..b0c984a --- /dev/null +++ b/Telegrator/MadiatorCore/Descriptors/HandlerDescriptor.cs @@ -0,0 +1,406 @@ +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Attributes.Components; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; + +namespace Telegrator.MadiatorCore.Descriptors +{ + /// + /// Specifies the type of handler descriptor. + /// + public enum DescriptorType + { + /// + /// General handler descriptor. + /// + General, + + /// + /// Keyed handler descriptor (uses a service key). + /// + Keyed, + + /// + /// Implicit handler descriptor. + /// + Implicit, + + /// + /// Singleton handler descriptor (single instance). + /// + Singleton + } + + /// + /// Describes a handler, its type, filters, and instantiation logic. + /// + public class HandlerDescriptor + { + /// + /// The type of the descriptor. + /// + public DescriptorType Type + { + get; + private set; + } + + /// + /// The type of the handler. + /// + public Type HandlerType + { + get; + private set; + } + + /// + /// The update type handled by this handler. + /// + public UpdateType UpdateType + { + get; + private set; + } + + /// + /// The indexer for handler concurrency and priority. + /// + public DescriptorIndexer Indexer + { + get; + set; + } + + /// + /// The set of filters associated with this handler. + /// + public DescriptorFiltersSet Filters + { + get; + private set; + } + + /// + /// The service key for keyed handlers. + /// + public object? ServiceKey + { + get; + private set; + } + + /// + /// Factory for creating handler instances. + /// + public Func? InstanceFactory + { + get; + set; + } + + /// + /// Singleton instance of the handler, if applicable. + /// + public UpdateHandlerBase? SingletonInstance + { + get; + set; + } + + /// + /// Display string for the handler (for debugging or logging). + /// + public string? DisplayString + { + get; + set; + } + + /// + /// Initializes a new instance of the class with the specified descriptor type and handler type. + /// Automatically inspects the handler type to extract attributes, filters, and configuration. + /// + /// The type of the descriptor + /// The type of the handler to describe + /// Thrown when the handler type is not compatible with the expected handler type + public HandlerDescriptor(DescriptorType descriptorType, Type handlerType) + { + UpdateHandlerAttributeBase handlerAttribute = HandlerInspector.GetHandlerAttribute(handlerType); + 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()))); + + StateKeeperAttributeBase? stateKeeperAttribute = HandlerInspector.GetStateKeeperAttribute(handlerType); + IFilter[] filters = HandlerInspector.GetFilterAttributes(handlerType, handlerAttribute.Type).ToArray(); + + Type = descriptorType; + HandlerType = handlerType; + UpdateType = handlerAttribute.Type; + Indexer = handlerAttribute.GetIndexer(); + Filters = new DescriptorFiltersSet(handlerAttribute, stateKeeperAttribute, filters); + } + + /// + /// Initializes a new instance of the class as a keyed handler with the specified service key. + /// + /// The type of the handler to describe + /// The service key for dependency injection + /// Thrown when is null + public HandlerDescriptor(Type handlerType, object serviceKey) : this(DescriptorType.Keyed, handlerType) + { + ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); + } + + /// + /// Initializes a new instance of the class with all basic properties. + /// + /// The type of the descriptor + /// The type of the handler + /// The type of update this handler processes + /// The indexer for handler concurrency and priority + /// The set of filters associated with this handler + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateType updateType, DescriptorIndexer indexer, DescriptorFiltersSet filters) + { + Type = type; + HandlerType = handlerType; + UpdateType = updateType; + Indexer = indexer; + Filters = filters; + } + + /// + /// Initializes a new instance of the class with singleton instance support. + /// + /// The type of the descriptor + /// The type of the handler + /// The type of update this handler processes + /// The indexer for handler concurrency and priority + /// The set of filters associated with this handler + /// The service key for dependency injection + /// The singleton instance of the handler + /// Thrown when or is null + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateType updateType, DescriptorIndexer indexer, DescriptorFiltersSet filters, object serviceKey, UpdateHandlerBase singletonInstance) + { + Type = type; + HandlerType = handlerType; + UpdateType = updateType; + Indexer = indexer; + Filters = filters; + ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); + SingletonInstance = singletonInstance ?? throw new ArgumentNullException(nameof(singletonInstance)); + } + + /// + /// Initializes a new instance of the class with instance factory support. + /// + /// The type of the descriptor + /// The type of the handler + /// The type of update this handler processes + /// The indexer for handler concurrency and priority + /// The set of filters associated with this handler + /// Factory for creating handler instances + /// Thrown when is null + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateType updateType, DescriptorIndexer indexer, DescriptorFiltersSet filters, Func instanceFactory) + { + Type = type; + HandlerType = handlerType; + UpdateType = updateType; + Indexer = indexer; + Filters = filters; + InstanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); + } + + /// + /// Initializes a new instance of the class with service key and instance factory support. + /// + /// The type of the descriptor + /// The type of the handler + /// The type of update this handler processes + /// The indexer for handler concurrency and priority + /// The set of filters associated with this handler + /// The service key for dependency injection + /// Factory for creating handler instances + /// Thrown when or is null + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateType updateType, DescriptorIndexer indexer, DescriptorFiltersSet filters, object serviceKey, Func instanceFactory) + { + Type = type; + HandlerType = handlerType; + UpdateType = updateType; + Indexer = indexer; + Filters = filters; + ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); + InstanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); + } + + /// + /// Initializes a new instance of the class with polling handler attribute and filters. + /// + /// The type of the descriptor + /// The type of the handler + /// The polling handler attribute containing configuration + /// Optional array of filters to apply + /// Optional state keeping filter + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateHandlerAttributeBase pollingHandlerAttribute, IFilter[]? filters, IFilter? stateKeepFilter) + { + Type = type; + HandlerType = handlerType; + UpdateType = pollingHandlerAttribute.Type; + Indexer = pollingHandlerAttribute.GetIndexer(); + Filters = new DescriptorFiltersSet(pollingHandlerAttribute, stateKeepFilter, filters); + } + + /// + /// Initializes a new instance of the class with polling handler attribute, filters, and singleton instance. + /// + /// The type of the descriptor + /// The type of the handler + /// The polling handler attribute containing configuration + /// Optional array of filters to apply + /// Optional state keeping filter + /// The service key for dependency injection + /// The singleton instance of the handler + /// Thrown when or is null + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateHandlerAttributeBase pollingHandlerAttribute, IFilter[]? filters, IFilter? stateKeepFilter, object serviceKey, UpdateHandlerBase singletonInstance) + { + Type = type; + HandlerType = handlerType; + UpdateType = pollingHandlerAttribute.Type; + Indexer = pollingHandlerAttribute.GetIndexer(); + Filters = new DescriptorFiltersSet(pollingHandlerAttribute, stateKeepFilter, filters); + ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); + SingletonInstance = singletonInstance ?? throw new ArgumentNullException(nameof(singletonInstance)); + } + + /// + /// Initializes a new instance of the class with polling handler attribute, filters, and instance factory. + /// + /// The type of the descriptor + /// The type of the handler + /// The polling handler attribute containing configuration + /// Optional array of filters to apply + /// Optional state keeping filter + /// Factory for creating handler instances + /// Thrown when is null + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateHandlerAttributeBase pollingHandlerAttribute, IFilter[]? filters, IFilter? stateKeepFilter, Func instanceFactory) + { + Type = type; + HandlerType = handlerType; + UpdateType = pollingHandlerAttribute.Type; + Indexer = pollingHandlerAttribute.GetIndexer(); + Filters = new DescriptorFiltersSet(pollingHandlerAttribute, stateKeepFilter, filters); + InstanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); + } + + /// + /// Initializes a new instance of the class with polling handler attribute, filters, service key, and instance factory. + /// + /// The type of the descriptor + /// The type of the handler + /// The polling handler attribute containing configuration + /// Optional array of filters to apply + /// Optional state keeping filter + /// The service key for dependency injection + /// Factory for creating handler instances + /// Thrown when or is null + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateHandlerAttributeBase pollingHandlerAttribute, IFilter[]? filters, IFilter? stateKeepFilter, object serviceKey, Func instanceFactory) + { + Type = type; + HandlerType = handlerType; + UpdateType = pollingHandlerAttribute.Type; + Indexer = pollingHandlerAttribute.GetIndexer(); + Filters = new DescriptorFiltersSet(pollingHandlerAttribute, stateKeepFilter, filters); + ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); + InstanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); + } + + /// + /// Initializes a new instance of the class with validation filter support. + /// + /// The type of the descriptor + /// The type of the handler + /// The type of update this handler processes + /// The indexer for handler concurrency and priority + /// Optional validation filter + /// Optional array of filters to apply + /// Optional state keeping filter + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateType updateType, DescriptorIndexer indexer, IFilter? validateFilter, IFilter[]? filters, IFilter? stateKeepFilter) + { + Type = type; + HandlerType = handlerType; + UpdateType = updateType; + Indexer = indexer; + Filters = new DescriptorFiltersSet(validateFilter, stateKeepFilter, filters); + } + + /// + /// Initializes a new instance of the class with validation filter and singleton instance support. + /// + /// The type of the descriptor + /// The type of the handler + /// The type of update this handler processes + /// The indexer for handler concurrency and priority + /// Optional validation filter + /// Optional array of filters to apply + /// Optional state keeping filter + /// The service key for dependency injection + /// The singleton instance of the handler + /// Thrown when or is null + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateType updateType, DescriptorIndexer indexer, IFilter? validateFilter, IFilter[]? filters, IFilter? stateKeepFilter, object serviceKey, UpdateHandlerBase singletonInstance) + { + Type = type; + HandlerType = handlerType; + UpdateType = updateType; + Indexer = indexer; + Filters = new DescriptorFiltersSet(validateFilter, stateKeepFilter, filters); + ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); + SingletonInstance = singletonInstance ?? throw new ArgumentNullException(nameof(singletonInstance)); + } + + /// + /// Initializes a new instance of the class with validation filter and instance factory support. + /// + /// The type of the descriptor + /// The type of the handler + /// The type of update this handler processes + /// The indexer for handler concurrency and priority + /// Optional validation filter + /// Optional array of filters to apply + /// Optional state keeping filter + /// Factory for creating handler instances + /// Thrown when is null + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateType updateType, DescriptorIndexer indexer, IFilter? validateFilter, IFilter[]? filters, IFilter? stateKeepFilter, Func instanceFactory) + { + Type = type; + HandlerType = handlerType; + UpdateType = updateType; + Indexer = indexer; + Filters = new DescriptorFiltersSet(validateFilter, stateKeepFilter, filters); + InstanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); + } + + /// + /// Initializes a new instance of the class with validation filter, service key, and instance factory support. + /// + /// The type of the descriptor + /// The type of the handler + /// The type of update this handler processes + /// The indexer for handler concurrency and priority + /// Optional validation filter + /// Optional array of filters to apply + /// Optional state keeping filter + /// The service key for dependency injection + /// Factory for creating handler instances + /// Thrown when or is null + public HandlerDescriptor(DescriptorType type, Type handlerType, UpdateType updateType, DescriptorIndexer indexer, IFilter? validateFilter, IFilter[]? filters, IFilter? stateKeepFilter, object serviceKey, Func instanceFactory) + { + Type = type; + HandlerType = handlerType; + UpdateType = updateType; + Indexer = indexer; + Filters = new DescriptorFiltersSet(validateFilter, stateKeepFilter, filters); + ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); + InstanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); + } + } +} diff --git a/Telegrator/MadiatorCore/Descriptors/HandlerDescriptorList.cs b/Telegrator/MadiatorCore/Descriptors/HandlerDescriptorList.cs new file mode 100644 index 0000000..7bc110d --- /dev/null +++ b/Telegrator/MadiatorCore/Descriptors/HandlerDescriptorList.cs @@ -0,0 +1,162 @@ +using System.Collections; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator; +using Telegrator.Configuration; +using Telegrator.MadiatorCore; + +namespace Telegrator.MadiatorCore.Descriptors +{ + /// + /// The collection containing the 's. Used to route 's in + /// + public sealed class HandlerDescriptorList : IEnumerable + { + private readonly object _lock = new object(); + private readonly SortedList _innerCollection; + private readonly IHandlersCollectingOptions? _options; + private readonly UpdateType _handlingType; + + private int count; + + /// + /// Gets a value indicating whether the collection is read-only. + /// + public bool IsReadOnly { get; private set; } = false; + + /// + /// Gets the of handlers in this collection. + /// + public UpdateType HandlingType => _handlingType; + + /// + /// Gets or sets the at the specified index. + /// + /// + /// + public HandlerDescriptor this[int index] + { + get => _innerCollection.Values[index]; + set => _innerCollection.Values[index] = value; + } + + /// + /// Initializes a new instance of the class without a specific . + /// + public HandlerDescriptorList() + : this(UpdateType.Unknown, default) { } + + /// + /// Initializes a new instance of the class. + /// + /// The update type for the handlers. + /// The collecting options. + public HandlerDescriptorList(UpdateType updateType, IHandlersCollectingOptions? options) + { + _innerCollection = []; + _handlingType = updateType; + _options = options; + } + + /// + /// Adds a new to the collection. + /// + /// The handler descriptor to add. + /// Thrown if the collection is frozen. + /// Thrown if the update type does not match. + public void Add(HandlerDescriptor descriptor) + { + lock (_lock) + { + if (IsReadOnly) + throw new CollectionFrozenException(); + + if (_handlingType != UpdateType.Unknown && descriptor.UpdateType != _handlingType) + throw new InvalidOperationException(); + + while (_innerCollection.TryGetValue(descriptor.Indexer, out HandlerDescriptor? conflictDescriptor)) + { + int newIndex = count++; + if (_options?.DescendDescriptorIndex ?? false) + newIndex += -1; + + descriptor.Indexer = descriptor.Indexer.UpdateIndex(count); + } + + _innerCollection.Add(descriptor.Indexer, descriptor); + } + } + + /// + /// Checks if the collection contains a with the specified . + /// + /// The descriptor indexer. + /// True if the descriptor exists; otherwise, false. + public bool ContainsKey(DescriptorIndexer indexer) + { + return _innerCollection.ContainsKey(indexer); + } + + /// + /// Removes the with the specified from the collection. + /// + /// The descriptor indexer. + /// True if the descriptor was removed; otherwise, false. + public bool Remove(DescriptorIndexer indexer) + { + lock (_lock) + { + return _innerCollection.Remove(indexer); + } + } + + /// + /// Removes the from the collection. + /// + /// + /// + public bool Remove(HandlerDescriptor descriptor) + { + lock (_lock) + { + int index = _innerCollection.IndexOfValue(descriptor); + if (index == -1) + return false; + + _innerCollection.RemoveAt(index); + return true; + } + } + + /// + /// Removes all descriptos from the + /// + public void Clear() + { + lock (_lock) + { + _innerCollection.Clear(); + } + } + + /// + /// Freezes the and prohibits adding new elements to it. + /// + public void Freeze() + { + IsReadOnly = true; + } + + /// + public IEnumerator GetEnumerator() + { + return _innerCollection.Values.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _innerCollection.Values.GetEnumerator(); + } + } +} diff --git a/Telegrator/MadiatorCore/Descriptors/HandlerInspector.cs b/Telegrator/MadiatorCore/Descriptors/HandlerInspector.cs new file mode 100644 index 0000000..bac3ea7 --- /dev/null +++ b/Telegrator/MadiatorCore/Descriptors/HandlerInspector.cs @@ -0,0 +1,73 @@ +using System.Reflection; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Attributes.Components; +using Telegrator.Filters.Components; + +namespace Telegrator.MadiatorCore.Descriptors +{ + /// + /// Provides methods for inspecting handler types and retrieving their attributes and filters. + /// + public static class HandlerInspector + { + /// + /// Gets the handler attribute from the specified member info. + /// + /// The member info representing the handler type. + /// The handler attribute. + public static UpdateHandlerAttributeBase GetHandlerAttribute(MemberInfo handlerType) + { + // Getting polling handler attribute + IEnumerable handlerAttrs = handlerType.GetCustomAttributes(); + + // + return handlerAttrs.Single(); + } + + /// + /// Gets the state keeper attribute from the specified member info, if present. + /// + /// The member info representing the handler type. + /// The state keeper attribute, or null if not present. + public static StateKeeperAttributeBase? GetStateKeeperAttribute(MemberInfo handlerType) + { + // Getting polling handler attribute + IEnumerable handlerAttrs = handlerType.GetCustomAttributes(); + + // + return handlerAttrs.Any() ? handlerAttrs.Single() : null; + } + + /// + /// Gets all filter attributes for the specified handler type and update type. + /// + /// The member info representing the handler type. + /// The valid update type. + /// An enumerable of filter attributes. + public static IEnumerable> GetFilterAttributes(MemberInfo handlerType, UpdateType validUpdType) + { + // + IEnumerable filters = handlerType.GetCustomAttributes(); + + // + if (filters.Any(filterAttr => !filterAttr.AllowedTypes.Contains(validUpdType))) + throw new InvalidOperationException(); + + UpdateFilterAttributeBase? lastFilterAttribute = null; + foreach (UpdateFilterAttributeBase filterAttribute in filters) + { + if (!filterAttribute.ProcessModifiers(lastFilterAttribute)) + { + lastFilterAttribute = null; + yield return filterAttribute.AnonymousFilter; + } + else + { + lastFilterAttribute = filterAttribute; + continue; + } + } + } + } +} diff --git a/Telegrator/MadiatorCore/Descriptors/HandlerLifetimeToken.cs b/Telegrator/MadiatorCore/Descriptors/HandlerLifetimeToken.cs new file mode 100644 index 0000000..bbafb27 --- /dev/null +++ b/Telegrator/MadiatorCore/Descriptors/HandlerLifetimeToken.cs @@ -0,0 +1,27 @@ +namespace Telegrator.MadiatorCore.Descriptors +{ + /// + /// Represents a token that tracks the lifetime of a handler instance. + /// + public class HandlerLifetimeToken + { + /// + /// Event triggered when the handler's lifetime has ended. + /// + public event Action? OnLifetimeEnded; + + /// + /// Gets a value indicating whether the handler's lifetime has ended. + /// + public bool IsEnded { get; private set; } + + /// + /// Marks the handler's lifetime as ended and triggers the event. + /// + public void LifetimeEnded() + { + IsEnded = true; + OnLifetimeEnded?.Invoke(this); + } + } +} diff --git a/Telegrator/MadiatorCore/IAwaitingProvider.cs b/Telegrator/MadiatorCore/IAwaitingProvider.cs new file mode 100644 index 0000000..8890f14 --- /dev/null +++ b/Telegrator/MadiatorCore/IAwaitingProvider.cs @@ -0,0 +1,17 @@ +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.MadiatorCore +{ + /// + /// Provider for managing awaiting handlers that can wait for specific update types. + /// + public interface IAwaitingProvider : IHandlersProvider + { + /// + /// Registers the usage of a handler and returns a disposable object to manage its lifetime. + /// + /// The to use. + /// An that manages the handler's usage lifetime. + public IDisposable UseHandler(HandlerDescriptor handlerDescriptor); + } +} diff --git a/Telegrator/MadiatorCore/ICollectingProvider.cs b/Telegrator/MadiatorCore/ICollectingProvider.cs new file mode 100644 index 0000000..af96c7c --- /dev/null +++ b/Telegrator/MadiatorCore/ICollectingProvider.cs @@ -0,0 +1,14 @@ +namespace Telegrator.MadiatorCore +{ + /// + /// Interface for providers that collect and manage handler collections. + /// Provides access to a collection of handlers for various processing operations. + /// + public interface ICollectingProvider + { + /// + /// Gets the collection of handlers managed by this provider. + /// + public IHandlersCollection Handlers { get; } + } +} diff --git a/Telegrator/MadiatorCore/IHandlersCollection.cs b/Telegrator/MadiatorCore/IHandlersCollection.cs new file mode 100644 index 0000000..d070bc0 --- /dev/null +++ b/Telegrator/MadiatorCore/IHandlersCollection.cs @@ -0,0 +1,70 @@ +using Telegram.Bot.Types.Enums; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.MadiatorCore +{ + /// + /// Collection class for managing handler descriptors organized by update type. + /// Provides functionality for collecting, adding, and organizing handlers. + /// + public interface IHandlersCollection + { + /// + /// Gets the collection of 's allowed by registered handlers + /// + public IEnumerable AllowedTypes { get; } + + /// + /// Gets the collection of keys for the handler lists. + /// + public IEnumerable Keys { get; } + + /// + /// Gets the collection of values. + /// + public IEnumerable Values { get; } + + /// + /// Gets the for the specified . + /// + /// The update type key. + /// The handler descriptor list for the given update type. + public HandlerDescriptorList this[UpdateType updateType] { get; } + + /// + /// Collects all handlers domain-wide and returns a new . + /// + /// A new with all handlers collected. + public IHandlersCollection CollectHandlersDomainWide(); + + /// + /// Adds a to the collection and returns the updated collection. + /// + /// The handler descriptor to add. + /// The updated . + public IHandlersCollection AddDescriptor(HandlerDescriptor descriptor); + + /// + /// Adds a handler of the specified type to the collection and returns the updated collection. + /// + /// The type of handler to add, must inherit from . + /// The updated . + public IHandlersCollection AddHandler() where THandler : UpdateHandlerBase; + + /// + /// Adds a handler of the specified type to the collection and returns the updated collection. + /// + /// The type of handler to add. + /// The updated . + /// Thrown if the handler type is invalid. + public IHandlersCollection AddHandler(Type handlerType); + + /// + /// Gets the for the specified . + /// + /// The handler descriptor. + /// The handler descriptor list containing the descriptor. + public HandlerDescriptorList GetDescriptorList(HandlerDescriptor descriptor); + } +} diff --git a/Telegrator/MadiatorCore/IHandlersProvider.cs b/Telegrator/MadiatorCore/IHandlersProvider.cs new file mode 100644 index 0000000..d6555e6 --- /dev/null +++ b/Telegrator/MadiatorCore/IHandlersProvider.cs @@ -0,0 +1,71 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.MadiatorCore +{ + /// + /// Provides methods to retrieve and describe handler information for updates. + /// + public interface IHandlersProvider + { + /// + /// Gets the collection of 's allowed by registered handlers + /// + public IEnumerable AllowedTypes { get; } + + /// + /// Gets the handlers for the specified update and context. + /// + /// The update router. + /// The Telegram bot client. + /// The update to handle. + /// + /// An enumerable of described handler info. + public IEnumerable GetHandlers(IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default); + + /// + /// Describes all handler descriptors in the list for the given context. + /// + /// The handler descriptor list. + /// The update router. + /// The Telegram bot client. + /// The update to handle. + /// + /// An enumerable of described handler info. + public IEnumerable DescribeDescriptors(HandlerDescriptorList descriptors, IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default); + + /// + /// Describes a single handler descriptor for the given context. + /// + /// The handler descriptor. + /// The update router. + /// The Telegram bot client. + /// The update to handle. + /// + /// The described handler info, or null if not applicable. + public DescribedHandlerInfo? DescribeHandler(HandlerDescriptor descriptor, IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default); + + /// + /// Gets an instance of the handler for the specified descriptor. + /// + /// The handler descriptor. + /// + /// The handler instance. + public UpdateHandlerBase GetHandlerInstance(HandlerDescriptor descriptor, CancellationToken cancellationToken = default); + + /// + /// Gets the list of bot commands supported by the provider. + /// + /// An enumerable of bot commands. + public IEnumerable GetBotCommands(CancellationToken cancellationToken = default); + + /// + /// Determines whether the provider contains any handlers. + /// + /// True if the provider is empty; otherwise, false. + public bool IsEmpty(); + } +} diff --git a/Telegrator/MadiatorCore/IPollingProvider.cs b/Telegrator/MadiatorCore/IPollingProvider.cs new file mode 100644 index 0000000..a96f954 --- /dev/null +++ b/Telegrator/MadiatorCore/IPollingProvider.cs @@ -0,0 +1,19 @@ +namespace Telegrator.MadiatorCore +{ + /// + /// Interface for polling providers that manage both regular and awaiting handlers. + /// Provides access to handlers for different types of update processing during polling operations. + /// + public interface IPollingProvider + { + /// + /// Gets the that manages handlers for polling. + /// + public IHandlersProvider HandlersProvider { get; } + + /// + /// Gets the that manages awaiting handlers for polling. + /// + public IAwaitingProvider AwaitingProvider { get; } + } +} diff --git a/Telegrator/MadiatorCore/IRouterExceptionHandler.cs b/Telegrator/MadiatorCore/IRouterExceptionHandler.cs new file mode 100644 index 0000000..f421523 --- /dev/null +++ b/Telegrator/MadiatorCore/IRouterExceptionHandler.cs @@ -0,0 +1,21 @@ +using Telegram.Bot; +using Telegram.Bot.Polling; + +namespace Telegrator.MadiatorCore +{ + /// + /// Interface for handling exceptions that occur during update routing operations. + /// Provides a centralized way to handle and log errors that occur during bot operation. + /// + public interface IRouterExceptionHandler + { + /// + /// Handles exceptions that occur during update routing. + /// + /// The instance. + /// The exception that occurred. + /// The indicating the source of the error. + /// The cancellation token. + public void HandleException(ITelegramBotClient botClient, Exception exception, HandleErrorSource source, CancellationToken cancellationToken); + } +} diff --git a/Telegrator/MadiatorCore/IUpdateHandlersPool.cs b/Telegrator/MadiatorCore/IUpdateHandlersPool.cs new file mode 100644 index 0000000..7547626 --- /dev/null +++ b/Telegrator/MadiatorCore/IUpdateHandlersPool.cs @@ -0,0 +1,49 @@ +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.MadiatorCore +{ + /// + /// Represents a delegate for when a handler is enqueued. + /// + /// The for the enqueued handler. + public delegate void HandlerEnqueued(DescribedHandlerInfo args); + /// + /// Represents a delegate for when a handler is executing. + /// + /// The for the executing handler. + public delegate void HandlerExecuting(DescribedHandlerInfo args); + + /// + /// Provides a pool for managing the execution and queuing of update handlers. + /// + public interface IUpdateHandlersPool : IDisposable + { + /// + /// Occurs when a handler is enqueued. + /// + public event HandlerEnqueued? HandlerEnqueued; + + /// + /// Occurs when a handler is executing. + /// + public event HandlerExecuting? HandlerExecuting; + + /// + /// Enqueues a collection of handlers for execution. + /// + /// The handlers to enqueue. + public void Enqueue(IEnumerable handlers); + + /// + /// Enqueues a single handler for execution. + /// + /// The handler to enqueue. + public void Enqueue(DescribedHandlerInfo handlerInfo); + + /// + /// Dequeues a handler using its lifetime token. + /// + /// The of the handler to dequeue. + public void Dequeue(HandlerLifetimeToken token); + } +} diff --git a/Telegrator/MadiatorCore/IUpdateRouter.cs b/Telegrator/MadiatorCore/IUpdateRouter.cs new file mode 100644 index 0000000..6992231 --- /dev/null +++ b/Telegrator/MadiatorCore/IUpdateRouter.cs @@ -0,0 +1,33 @@ +using Telegram.Bot.Polling; +using Telegrator.Configuration; +using Telegrator.Handlers.Components; + +namespace Telegrator.MadiatorCore +{ + /// + /// Interface for update routers that handle incoming updates and manage handler execution. + /// Combines update handling capabilities with polling provider functionality and exception handling. + /// + public interface IUpdateRouter : IUpdateHandler, IPollingProvider + { + /// + /// Gets the for the router. + /// + public TelegramBotOptions Options { get; } + + /// + /// Gets the that manages handler execution. + /// + public IUpdateHandlersPool HandlersPool { get; } + + /// + /// Gets or sets the for handling exceptions. + /// + public IRouterExceptionHandler? ExceptionHandler { get; set; } + + /// + /// Default hand;er container factory + /// + public IHandlerContainerFactory? DefaultContainerFactory { get; set; } + } +} diff --git a/Telegrator/Polling/ReactiveUpdateReceiver.cs b/Telegrator/Polling/ReactiveUpdateReceiver.cs new file mode 100644 index 0000000..a4d5b68 --- /dev/null +++ b/Telegrator/Polling/ReactiveUpdateReceiver.cs @@ -0,0 +1,85 @@ +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Requests; +using Telegram.Bot.Types; + +namespace Telegrator.Polling +{ + /// + /// Reactive implementation of for polling updates from Telegram. + /// Provides custom update receiving logic with error handling and configuration options. + /// + /// The Telegram bot client for making API requests. + /// Optional receiver options for configuring update polling behavior. + public class ReactiveUpdateReceiver(ITelegramBotClient client, ReceiverOptions? options) : IUpdateReceiver + { + /// + /// Gets the receiver options for configuring update polling behavior. + /// + public readonly ReceiverOptions? Options = options; + + /// + /// Gets the Telegram bot client for making API requests. + /// + public readonly ITelegramBotClient Client = client; + + /// + /// Receives updates from Telegram using long polling. + /// Handles update processing, error handling, and cancellation. + /// + /// The update handler to process received updates. + /// The cancellation token to stop receiving updates. + /// A task representing the asynchronous update receiving operation. + public async Task ReceiveAsync(IUpdateHandler updateHandler, CancellationToken cancellationToken) + { + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken).Token; + GetUpdatesRequest request = new GetUpdatesRequest() + { + AllowedUpdates = Options?.AllowedUpdates ?? [], + Limit = Options?.Limit.GetValueOrDefault(100), + Offset = Options?.Offset + }; + + if (Options?.DropPendingUpdates ?? false) + { + try + { + Update[] array = await Client.GetUpdates(-1, 1, 0, [], cancellationToken).ConfigureAwait(false); + request.Offset = array.Length != 0 ? array[^1].Id + 1 : 0; + } + catch (OperationCanceledException) + { + return; + } + } + + while (!cancellationToken.IsCancellationRequested) + { + try + { + request.Timeout = (int)Client.Timeout.TotalSeconds; + foreach (Update update in await Client.SendRequest(request, cancellationToken).ConfigureAwait(false)) + { + try + { + request.Offset = update.Id + 1; + await updateHandler.HandleUpdateAsync(Client, update, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); + } + catch (Exception exception2) + { + await updateHandler.HandleErrorAsync(Client, exception2, HandleErrorSource.HandleUpdateError, cancellationToken).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + return; + } + catch (Exception exception) + { + await updateHandler.HandleErrorAsync(Client, exception, HandleErrorSource.PollingError, cancellationToken).ConfigureAwait(false); + } + } + } + } +} diff --git a/Telegrator/Polling/UpdateHandlersPool.cs b/Telegrator/Polling/UpdateHandlersPool.cs new file mode 100644 index 0000000..c4a029d --- /dev/null +++ b/Telegrator/Polling/UpdateHandlersPool.cs @@ -0,0 +1,238 @@ +using System.Collections.Concurrent; +using Telegrator.Configuration; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Polling +{ + /// + /// Implementation of that manages the execution of handlers. + /// Provides thread-safe queuing and execution of handlers with configurable concurrency limits. + /// + public class UpdateHandlersPool : IUpdateHandlersPool + { + /// + /// Synchronization object for thread-safe operations. + /// + protected object SyncObj = new object(); + + /// + /// Event that signals when awaiting handlers are queued. + /// + protected ManualResetEventSlim AwaitingHandlersQueuedEvent = null!; + + /// + /// Semaphore for controlling the number of concurrently executing handlers. + /// + protected SemaphoreSlim ExecutingHandlersSemaphore = null!; + + /// + /// Queue for storing awaiting handlers. + /// + protected readonly ConcurrentQueue AwaitingHandlersQueue = []; + + /// + /// Dictionary for tracking currently executing handlers. + /// + protected readonly ConcurrentDictionary ExecutingHandlersPool = []; + + /// + /// The bot configuration options. + /// + protected readonly TelegramBotOptions Options; + + /// + /// The global cancellation token for stopping all operations. + /// + protected readonly CancellationToken GlobalCancellationToken; + + /// + /// Flag indicating whether the pool has been disposed. + /// + protected bool disposed = false; + + /// + public event HandlerEnqueued? HandlerEnqueued; + + /// + public event HandlerExecuting? HandlerExecuting; + + /// + /// Initializes a new instance of the class. + /// + /// The bot configuration options. + /// The global cancellation token. + public UpdateHandlersPool(TelegramBotOptions options, CancellationToken globalCancellationToken) + { + Options = options; + GlobalCancellationToken = globalCancellationToken; + + if (options.MaximumParallelWorkingHandlers != null) + { + ExecutingHandlersSemaphore = new SemaphoreSlim(options.MaximumParallelWorkingHandlers ?? 0); + AwaitingHandlersQueuedEvent = new ManualResetEventSlim(false); + } + + if (Options.MaximumParallelWorkingHandlers != null) + HandlersCheckpoint(); + } + + /// + public void Enqueue(IEnumerable handlers) + { + handlers.ForEach(Enqueue); + } + + /// + public void Enqueue(DescribedHandlerInfo handlerInfo) + { + if (Options.MaximumParallelWorkingHandlers == null) + { + Task.Run(async () => await ExecuteHandlerWrapper(handlerInfo)); + return; + } + + lock (SyncObj) + { + AwaitingHandlersQueue.Enqueue(handlerInfo); + HandlerEnqueued?.Invoke(handlerInfo); + AwaitingHandlersQueuedEvent.Set(); + } + } + + /// + public void Dequeue(HandlerLifetimeToken token) + { + if (Options.MaximumParallelWorkingHandlers == null) + return; + + lock (SyncObj) + { + ExecutingHandlersPool.TryRemove(token, out _); + ExecutingHandlersSemaphore.Release(1); + } + } + + /// + /// Main checkpoint method that manages handler execution in a loop. + /// Continuously processes queued handlers while respecting concurrency limits. + /// + protected virtual async void HandlersCheckpoint() + { + await Task.Yield(); + while (!GlobalCancellationToken.IsCancellationRequested) + { + if (!CanEnqueueHandler()) + { + await ExecutingHandlersSemaphore.WaitAsync(GlobalCancellationToken); + if (!CanEnqueueHandler()) + continue; + } + + if (!TryDequeueHandler(out DescribedHandlerInfo? enqueuedHandler)) + { + AwaitingHandlersQueuedEvent.Reset(); + AwaitingHandlersQueuedEvent.Wait(GlobalCancellationToken); + + if (!TryDequeueHandler(out enqueuedHandler)) + continue; + } + + if (enqueuedHandler == null) + continue; + + ExecuteHandler(enqueuedHandler); + } + } + + /// + /// Executes a handler by creating a lifetime token and tracking the execution. + /// + /// The handler to execute. + protected virtual void ExecuteHandler(DescribedHandlerInfo enqueuedHandler) + { + HandlerLifetimeToken lifetimeToken = enqueuedHandler.HandlerLifetime; + lifetimeToken.OnLifetimeEnded += Dequeue; + + Task executingHandler = ExecuteHandlerWrapper(enqueuedHandler); + lock (SyncObj) + ExecutingHandlersPool.TryAdd(lifetimeToken, executingHandler); + + HandlerExecuting?.Invoke(enqueuedHandler); + } + + /// + /// Wrapper method that executes a handler and handles exceptions. + /// + /// The handler to execute. + /// A task representing the asynchronous execution. + /// Thrown when the handler execution fails. + protected virtual async Task ExecuteHandlerWrapper(DescribedHandlerInfo enqueuedHandler) + { + try + { + await enqueuedHandler.Execute(GlobalCancellationToken); + } + catch (OperationCanceledException) + { + return; + } + catch (Exception ex) + { + throw new HandlerFaultedException(enqueuedHandler, ex); + } + } + + /// + /// Checks if a new handler can be enqueued based on the current execution count. + /// + /// True if a new handler can be enqueued; otherwise, false. + protected virtual bool CanEnqueueHandler() + { + lock (SyncObj) + { + return ExecutingHandlersPool.Count < Options.MaximumParallelWorkingHandlers; + } + } + + /// + /// Attempts to dequeue a handler from the awaiting queue. + /// + /// The dequeued handler, if successful. + /// True if a handler was successfully dequeued; otherwise, false. + protected virtual bool TryDequeueHandler(out DescribedHandlerInfo? enqueuedHandler) + { + lock (SyncObj) + { + return AwaitingHandlersQueue.TryDequeue(out enqueuedHandler); + } + } + + /// + /// Disposes of the handlers pool and releases all resources. + /// + public virtual void Dispose() + { + if (disposed) + return; + + if (ExecutingHandlersSemaphore != null) + { + ExecutingHandlersSemaphore.Dispose(); + ExecutingHandlersSemaphore = null!; + } + + if (AwaitingHandlersQueuedEvent != null) + { + AwaitingHandlersQueuedEvent.Dispose(); + AwaitingHandlersQueuedEvent = null!; + } + + if (SyncObj != null) + SyncObj = null!; + + GC.SuppressFinalize(this); + disposed = true; + } + } +} diff --git a/Telegrator/Polling/UpdateRouter.cs b/Telegrator/Polling/UpdateRouter.cs new file mode 100644 index 0000000..43fc3b4 --- /dev/null +++ b/Telegrator/Polling/UpdateRouter.cs @@ -0,0 +1,125 @@ +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegrator.Polling; +using Telegrator.Configuration; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Polling +{ + /// + /// Implementation of that routes updates to appropriate handlers. + /// Manages the distribution of updates between regular handlers and awaiting handlers. + /// + public class UpdateRouter : IUpdateRouter + { + /// + /// The bot configuration options. + /// + private readonly TelegramBotOptions _options; + + /// + /// The provider for regular handlers. + /// + private readonly IHandlersProvider _handlersProvider; + + /// + /// The provider for awaiting handlers. + /// + private readonly IAwaitingProvider _awaitingProvider; + + /// + /// The pool for managing handler execution. + /// + private readonly IUpdateHandlersPool _HandlersPool; + + /// + public IHandlersProvider HandlersProvider => _handlersProvider; + + /// + public IAwaitingProvider AwaitingProvider => _awaitingProvider; + + /// + public TelegramBotOptions Options => _options; + + /// + public IUpdateHandlersPool HandlersPool => _HandlersPool; + + /// + public IRouterExceptionHandler? ExceptionHandler { get; set; } + + /// + public IHandlerContainerFactory? DefaultContainerFactory { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The provider for regular handlers. + /// The provider for awaiting handlers. + /// The bot configuration options. + public UpdateRouter(IHandlersProvider handlersProvider, IAwaitingProvider awaitingProvider, TelegramBotOptions options) + { + _options = options; + _handlersProvider = handlersProvider; + _awaitingProvider = awaitingProvider; + _HandlersPool = new UpdateHandlersPool(_options, _options.GlobalCancellationToken); + } + + /// + /// Initializes a new instance of the class with a custom handlers pool. + /// + /// The provider for regular handlers. + /// The provider for awaiting handlers. + /// The bot configuration options. + /// The custom handlers pool to use. + public UpdateRouter(IHandlersProvider handlersProvider, IAwaitingProvider awaitingProvider, TelegramBotOptions options, IUpdateHandlersPool handlersPool) + { + _options = options; + _handlersProvider = handlersProvider; + _awaitingProvider = awaitingProvider; + _HandlersPool = handlersPool; + } + + /// + /// Handles errors that occur during update processing. + /// + /// The Telegram bot client. + /// The exception that occurred. + /// The source of the error. + /// The cancellation token. + /// A task representing the asynchronous error handling operation. + public virtual Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, HandleErrorSource source, CancellationToken cancellationToken) + { + ExceptionHandler?.HandleException(botClient, exception, source, cancellationToken); + return Task.CompletedTask; + } + + /// + /// Handles incoming updates by routing them to appropriate handlers. + /// + /// The Telegram bot client. + /// The update to handle. + /// The cancellation token. + /// A task representing the asynchronous update handling operation. + public virtual Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) + { + // Queuing handlers for execution + foreach (DescribedHandlerInfo handler in GetHandlers(botClient, update, cancellationToken)) + HandlersPool.Enqueue(handler); + + return Task.CompletedTask; + } + + private IEnumerable GetHandlers(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) + { + // Getting handlers in update awaiting pool + IEnumerable handlers = AwaitingProvider.GetHandlers(this, botClient, update, cancellationToken); + if (handlers.Any() && Options.ExclusiveAwaitingHandlerRouting) + return handlers; + + return handlers.Concat(HandlersProvider.GetHandlers(this, botClient, update, cancellationToken)); + } + } +} diff --git a/Telegrator/Providers/AwaitingProvider.cs b/Telegrator/Providers/AwaitingProvider.cs new file mode 100644 index 0000000..ad82f8d --- /dev/null +++ b/Telegrator/Providers/AwaitingProvider.cs @@ -0,0 +1,65 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegrator.Configuration; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Providers +{ + /// + /// Provider for managing awaiting handlers that can wait for specific update types. + /// Extends HandlersProvider to provide functionality for creating and managing awaiter handlers. + /// + /// The bot configuration options. + /// The bot information. + public class AwaitingProvider(TelegramBotOptions options, ITelegramBotInfo botInfo) : HandlersProvider([], options, botInfo), IAwaitingProvider + { + /// + /// List of handler descriptors for awaiting handlers. + /// + protected readonly HandlerDescriptorList HandlersList = []; + + /// + public override IEnumerable GetHandlers(IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default) + { + return DescribeDescriptors(HandlersList, updateRouter, client, update, cancellationToken); + } + + /// + public IDisposable UseHandler(HandlerDescriptor handlerDescriptor) + { + HandlerToken handlerToken = new HandlerToken(HandlersList, handlerDescriptor); + handlerToken.Register(); + return handlerToken; + } + + /// + /// Token for managing the lifetime of a handler in the awaiting provider. + /// Implements IDisposable to automatically remove the handler when disposed. + /// + /// The list of handler descriptors. + /// The handler descriptor to manage. + private readonly struct HandlerToken(HandlerDescriptorList handlersList, HandlerDescriptor handlerDescriptor) : IDisposable + { + /// + /// Registers the handler descriptor in the handlers list. + /// + /// Thrown when the handler descriptor has no singleton instance. + public readonly void Register() + { + if (handlerDescriptor.SingletonInstance == null) + throw new Exception(); + + handlersList.Add(handlerDescriptor); + } + + /// + /// Disposes of the handler token by removing the handler descriptor from the list. + /// + public readonly void Dispose() + { + handlersList.Remove(handlerDescriptor.Indexer); + } + } + } +} diff --git a/Telegrator/Providers/HandlersCollection.cs b/Telegrator/Providers/HandlersCollection.cs new file mode 100644 index 0000000..38a4bd9 --- /dev/null +++ b/Telegrator/Providers/HandlersCollection.cs @@ -0,0 +1,194 @@ +using System.Reflection; +using Telegram.Bot.Types.Enums; +using Telegrator; +using Telegrator.Annotations; +using Telegrator.Attributes; +using Telegrator.Configuration; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Providers +{ + /// + /// Collection class for managing handler descriptors organized by update type. + /// Provides functionality for collecting, adding, and organizing handlers. + /// + /// Optional configuration options for handler collecting. + public class HandlersCollection(IHandlersCollectingOptions? options) : IHandlersCollection + { + private readonly List _allowedTypes = []; + + /// + /// Dictionary that organizes handler descriptors by update type. + /// + protected readonly Dictionary InnerDictionary = []; + + /// + /// Configuration options for handler collecting. + /// + protected readonly IHandlersCollectingOptions? Options = options; + + /// + /// Gets whether handlers must have a parameterless constructor. + /// + protected virtual bool MustHaveParameterlessCtor => true; + + /// + /// List of command aliases that have been registered. + /// + public readonly List CommandAliasses = []; + + /// + public IEnumerable AllowedTypes => _allowedTypes; + + /// + public IEnumerable Keys + { + get => InnerDictionary.Keys; + } + + /// + public IEnumerable Values + { + get => InnerDictionary.Values; + } + + /// + public HandlerDescriptorList this[UpdateType updateType] + { + get => InnerDictionary[updateType]; + } + + /// + /// + /// Collects all handlers from the entry assembly domain-wide. + /// Scans for types that implement handlers and adds them to the collection. + /// + /// This collection instance for method chaining. + /// Thrown when the entry assembly cannot be found. + public virtual IHandlersCollection CollectHandlersDomainWide() + { + Assembly? entryAssembly = Assembly.GetEntryAssembly() ?? throw new Exception(); + entryAssembly.GetExportedTypes() + .Where(type => type.GetCustomAttribute() == null) + .Where(type => type.IsHandlerRealization()) + .ForEach(type => AddHandler(type)); + + return this; + } + + /// + /// + /// Adds a handler descriptor to the collection. + /// + /// The handler descriptor to add. + /// This collection instance for method chaining. + /// Thrown when the handler type doesn't have a parameterless constructor and MustHaveParameterlessCtor is true. + public virtual IHandlersCollection AddDescriptor(HandlerDescriptor descriptor) + { + if (MustHaveParameterlessCtor && !descriptor.HandlerType.HasParameterlessCtor()) + throw new Exception(); + + _allowedTypes.Union(descriptor.UpdateType); + MightAwaitAttribute[] mightAwaits = descriptor.HandlerType.GetCustomAttributes().ToArray(); + if (mightAwaits.Length > 0) + _allowedTypes.Union(mightAwaits.SelectMany(attr => attr.UpdateTypes)); + + IntersectCommands(descriptor); + GetDescriptorList(descriptor).Add(descriptor); + return this; + } + + /// + /// + /// Adds a handler type to the collection. + /// + /// The type of handler to add. + /// This collection instance for method chaining. + public virtual IHandlersCollection AddHandler() where THandler : UpdateHandlerBase + { + AddHandler(typeof(THandler)); + return this; + } + + /// + /// + /// Adds a handler type to the collection. + /// + /// The type of handler to add. + /// This collection instance for method chaining. + /// Thrown when the type is not a valid handler implementation. + public virtual IHandlersCollection AddHandler(Type handlerType) + { + if (!handlerType.IsHandlerRealization()) + throw new Exception(); + + if (handlerType.IsCustomDescriptorsProvider()) + { + foreach (HandlerDescriptor handlerDescriptor in InvokeCustomDescriptorsProvider(handlerType)) + AddDescriptor(handlerDescriptor); + } + else + { + HandlerDescriptor descriptor = new HandlerDescriptor(DescriptorType.General, handlerType); + AddDescriptor(descriptor); + } + + return this; + } + + /// + /// + /// Gets or creates a descriptor list for the specified update type. + /// + /// The handler descriptor to get the list for. + /// The descriptor list for the update type. + public virtual HandlerDescriptorList GetDescriptorList(HandlerDescriptor descriptor) + { + if (!InnerDictionary.TryGetValue(descriptor.UpdateType, out HandlerDescriptorList? list)) + { + list = new HandlerDescriptorList(descriptor.UpdateType, Options); + InnerDictionary.Add(descriptor.UpdateType, list); + } + + return list; + } + + /// + /// + /// Checks for intersecting command aliases and handles them according to configuration. + /// + /// The handler descriptor to check for command aliases. + /// Thrown when intersecting command aliases are found and ExceptIntersectingCommandAliases is enabled. + protected void IntersectCommands(HandlerDescriptor descriptor) + { + if (Options == null) + return; + + CommandAlliasAttribute? alliasAttribute = descriptor.HandlerType.GetCustomAttribute(); + if (alliasAttribute == null) + return; + + if (Options.ExceptIntersectingCommandAliases && CommandAliasses.Intersect(alliasAttribute.Alliases, StringComparer.InvariantCultureIgnoreCase).Any()) + throw new Exception(descriptor.HandlerType.FullName); + + CommandAliasses.AddRange(alliasAttribute.Alliases); + } + + /// + /// Invokes a custom descriptors provider to get handler descriptors. + /// + /// The handler type that implements ICustomDescriptorsProvider. + /// A collection of handler descriptors from the custom provider. + /// Thrown when the handler type doesn't have a parameterless constructor or cannot be instantiated. + protected virtual IEnumerable InvokeCustomDescriptorsProvider(Type handlerType) + { + if (!handlerType.HasParameterlessCtor()) + throw new Exception(); + + ICustomDescriptorsProvider? provider = (ICustomDescriptorsProvider?)Activator.CreateInstance(handlerType); + return provider == null ? throw new Exception() : provider.DescribeHandlers(); + } + } +} diff --git a/Telegrator/Providers/HandlersProvider.cs b/Telegrator/Providers/HandlersProvider.cs new file mode 100644 index 0000000..fe9d1da --- /dev/null +++ b/Telegrator/Providers/HandlersProvider.cs @@ -0,0 +1,204 @@ +using System.Collections.ObjectModel; +using System.Reflection; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegrator.Annotations; +using Telegrator.Configuration; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore; +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Providers +{ + /// + /// Provides handler resolution and instantiation logic for Telegram bot updates. + /// Responsible for mapping update types to handler descriptors, filtering handlers based on update context, + /// and creating handler instances with appropriate lifecycle management. + /// + public class HandlersProvider : IHandlersProvider + { + /// + public IEnumerable AllowedTypes { get; } + + /// + /// Read-only dictionary mapping to lists of handler descriptors. + /// Each descriptor list is frozen to prevent modification after initialization. + /// + protected readonly ReadOnlyDictionary HandlersDictionary; + + /// + /// Configuration options for the bot and handler execution behavior. + /// + protected readonly TelegramBotOptions Options; + + /// + /// Information about the Telegram bot instance, used for filter context creation. + /// + protected readonly ITelegramBotInfo BotInfo; + + /// + /// Initializes a new instance of with the specified handler collections and configuration. + /// + /// Collection of handler descriptor lists organized by update type + /// Configuration options for the bot and handler execution + /// Information about the Telegram bot instance + /// Thrown when options or botInfo is null + public HandlersProvider(IHandlersCollection handlers, TelegramBotOptions options, ITelegramBotInfo botInfo) + { + AllowedTypes = handlers.AllowedTypes; + HandlersDictionary = handlers.Values.ForEach(list => list.Freeze()).ToReadOnlyDictionary(list => list.HandlingType); + Options = options ?? throw new ArgumentNullException(nameof(options)); + BotInfo = botInfo ?? throw new ArgumentNullException(nameof(botInfo)); + } + + /// + /// Initializes a new instance of with the specified handler collections and configuration. + /// + /// Collection of handler descriptor lists organized by update type + /// Configuration options for the bot and handler execution + /// Information about the Telegram bot instance + /// Thrown when options or botInfo is null + public HandlersProvider(IEnumerable handlers, TelegramBotOptions options, ITelegramBotInfo botInfo) + { + AllowedTypes = Update.AllTypes; + HandlersDictionary = handlers.ForEach(list => list.Freeze()).ToReadOnlyDictionary(list => list.HandlingType); + Options = options ?? throw new ArgumentNullException(nameof(options)); + BotInfo = botInfo ?? throw new ArgumentNullException(nameof(botInfo)); + } + + /// + /// Gets the handlers that match the specified update, using the provided router and client. + /// Searches for handlers by update type, falling back to Unknown type if no specific handlers are found. + /// + /// The update router for handler execution + /// The Telegram bot client instance + /// The incoming Telegram update to process + /// + /// A collection of described handler information for the update + public virtual IEnumerable GetHandlers(IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default) + { + if (!HandlersDictionary.TryGetValue(update.Type, out HandlerDescriptorList? descriptors)) + { + if (!HandlersDictionary.TryGetValue(UpdateType.Unknown, out descriptors)) + return []; + } + + if (descriptors == null || !descriptors.Any()) + return []; + + return DescribeDescriptors(descriptors, updateRouter, client, update, cancellationToken); + } + + /// + /// Describes all handler descriptors for a given update context. + /// Processes descriptors in reverse order and respects the ExecuteOnlyFirstFoundHanlder option. + /// + /// The list of handler descriptors to process + /// The update router for handler execution + /// The Telegram bot client instance + /// The incoming Telegram update to process + /// + /// A collection of described handler information + public virtual IEnumerable DescribeDescriptors(HandlerDescriptorList descriptors, IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default) + { + foreach (HandlerDescriptor descriptor in descriptors.Reverse()) + { + cancellationToken.ThrowIfCancellationRequested(); + DescribedHandlerInfo? describedHandler = DescribeHandler(descriptor, updateRouter, client, update, cancellationToken); + if (describedHandler == null) + continue; + + yield return describedHandler; + if (Options.ExecuteOnlyFirstFoundHanlder) + break; + } + } + + /// + /// Describes a single handler descriptor for a given update context. + /// Validates the handler's filters against the update and creates a handler instance if validation passes. + /// + /// The handler descriptor to process + /// The update router for handler execution + /// The Telegram bot client instance + /// The incoming Telegram update to process + /// + /// The described handler info if validation passes; otherwise, null + public virtual DescribedHandlerInfo? DescribeHandler(HandlerDescriptor descriptor, IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + FilterExecutionContext filterContext = new FilterExecutionContext(BotInfo, update, update); + if (!descriptor.Filters.Validate(filterContext)) + return null; + + UpdateHandlerBase handlerInstance = GetHandlerInstance(descriptor, cancellationToken); + return new DescribedHandlerInfo(updateRouter, client, handlerInstance, filterContext, descriptor.DisplayString); + } + + /// + /// Instantiates a handler for the given descriptor, using the appropriate creation strategy based on descriptor type. + /// Supports singleton, implicit, keyed, and general descriptor types with different instantiation patterns. + /// + /// The handler descriptor containing type and instantiation information + /// + /// An instance of for the descriptor + /// Thrown when the descriptor type is not recognized + public virtual UpdateHandlerBase GetHandlerInstance(HandlerDescriptor descriptor, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + switch (descriptor.Type) + { + case DescriptorType.Implicit: + case DescriptorType.Singleton: + { + return descriptor.SingletonInstance ??= (descriptor.InstanceFactory != null + ? descriptor.SingletonInstance = descriptor.InstanceFactory.Invoke() + : descriptor.SingletonInstance = (UpdateHandlerBase)Activator.CreateInstance(descriptor.HandlerType, [descriptor.UpdateType])); + } + + case DescriptorType.Keyed: + case DescriptorType.General: + { + return descriptor.InstanceFactory == null + ? (UpdateHandlerBase)Activator.CreateInstance(descriptor.HandlerType, [descriptor.UpdateType]) + : descriptor.InstanceFactory.Invoke(); + } + + default: + throw new Exception(); + } + } + + /// + /// Gets the list of bot commands defined by all handler types with . + /// Extracts command aliases and descriptions from message handlers for bot command registration. + /// + /// + /// A collection of objects for the bot + public IEnumerable GetBotCommands(CancellationToken cancellationToken = default) + { + if (!HandlersDictionary.TryGetValue(UpdateType.Message, out HandlerDescriptorList? list)) + yield break; + + foreach (BotCommand botCommand in list + .Select(descriptor => descriptor.HandlerType) + .SelectMany(handlerType => handlerType.GetCustomAttributes() + .SelectMany(attribute => attribute.Alliases.Select(alias => new BotCommand(alias, attribute.Description))))) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return botCommand; + } + } + + /// + /// Determines whether the provider contains any handlers. + /// + /// True if there are no handlers registered; otherwise, false + public virtual bool IsEmpty() + { + return HandlersDictionary.Count == 0; + } + } +} diff --git a/Telegrator/Providers/ICustomDescriptorsProvider.cs b/Telegrator/Providers/ICustomDescriptorsProvider.cs new file mode 100644 index 0000000..9e3e4bd --- /dev/null +++ b/Telegrator/Providers/ICustomDescriptorsProvider.cs @@ -0,0 +1,17 @@ +using Telegrator.MadiatorCore.Descriptors; + +namespace Telegrator.Providers +{ + /// + /// Interface for classes that can provide custom handler descriptors. + /// Allows classes to define their own handler description logic beyond the standard reflection-based approach. + /// + public interface ICustomDescriptorsProvider + { + /// + /// Describes the handlers provided by this class. + /// + /// A collection of handler descriptors. + public IEnumerable DescribeHandlers(); + } +} diff --git a/Telegrator/StateKeeping/ArrayStateKeeper.cs b/Telegrator/StateKeeping/ArrayStateKeeper.cs new file mode 100644 index 0000000..120cf01 --- /dev/null +++ b/Telegrator/StateKeeping/ArrayStateKeeper.cs @@ -0,0 +1,59 @@ +using Telegrator.StateKeeping.Components; + +namespace Telegrator.StateKeeping +{ + /// + /// 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. + /// + /// The type of the key used to identify state contexts. + /// The type of the state values. Must be non-null. + /// The array of states that define the allowed state sequence. + public abstract class ArrayStateKeeper(params TState[] states) : StateKeeperBase where TState : notnull where TKey : notnull + { + /// + /// The array of states that defines the allowed state sequence for navigation. + /// + protected readonly TState[] ArrayStates = states; + + /// + /// Moves to the previous state in the array sequence. + /// + /// The current state to move backward from. + /// The key parameter (unused in this implementation). + /// The previous state in the array sequence. + /// Thrown when the current state is not found in the array. + /// Thrown when trying to move backward from the first state. + 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]; + } + + /// + /// Moves to the next state in the array sequence. + /// + /// The current state to move forward from. + /// The key parameter (unused in this implementation). + /// The next state in the array sequence. + /// Thrown when the current state is not found in the array. + /// Thrown when trying to move forward from the last state. + 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]; + } + } +} diff --git a/Telegrator/StateKeeping/ChatIdResolver.cs b/Telegrator/StateKeeping/ChatIdResolver.cs new file mode 100644 index 0000000..07d8e8e --- /dev/null +++ b/Telegrator/StateKeeping/ChatIdResolver.cs @@ -0,0 +1,21 @@ +using Telegram.Bot.Types; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.StateKeeping +{ + /// + /// 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. + /// + public class ChatIdResolver : IStateKeyResolver + { + /// + /// Resolves the chat ID from a Telegram update. + /// + /// The Telegram update to extract the chat ID from. + /// The chat ID as a long value. + /// Thrown when the update does not contain a valid chat ID. + public long ResolveKey(Update keySource) + => keySource.GetChatId() ?? throw new ArgumentException("Cannot resolve ChatID for this Update"); + } +} diff --git a/Telegrator/StateKeeping/Components/IStateKeyResolver.cs b/Telegrator/StateKeeping/Components/IStateKeyResolver.cs new file mode 100644 index 0000000..e78395c --- /dev/null +++ b/Telegrator/StateKeeping/Components/IStateKeyResolver.cs @@ -0,0 +1,18 @@ +using Telegram.Bot.Types; + +namespace Telegrator.StateKeeping.Components +{ + /// + /// Defines a resolver for extracting a key from an update for state keeping purposes. + /// + /// The type of the key. + public interface IStateKeyResolver where TKey : notnull + { + /// + /// Resolves a key from the specified . + /// + /// The update to resolve the key from. + /// The resolved key. + public TKey ResolveKey(Update keySource); + } +} diff --git a/Telegrator/StateKeeping/Components/StateKeeperBase.cs b/Telegrator/StateKeeping/Components/StateKeeperBase.cs new file mode 100644 index 0000000..8f0b84b --- /dev/null +++ b/Telegrator/StateKeeping/Components/StateKeeperBase.cs @@ -0,0 +1,143 @@ +using Telegram.Bot.Types; + +namespace Telegrator.StateKeeping.Components +{ + /// + /// Base class for managing state associated with updates and keys. + /// + /// The type of the key used for state resolution. + /// The type of the state. + public abstract class StateKeeperBase where TState : notnull where TKey : notnull + { + private readonly Dictionary States = []; + + /// + /// Gets or sets the key resolver used to resolve keys from updates. + /// + public IStateKeyResolver KeyResolver { get; set; } = null!; + + /// + /// Gets the default state value. + /// + public abstract TState DefaultState { get; } + + /// + /// Sets the state for the specified update. + /// + /// The update to use as a key source. + /// The new state value. + public void SetState(Update keySource, TState newState) + { + TKey key = KeyResolver.ResolveKey(keySource); + States.Set(key, newState, DefaultState); + } + + /// + /// Gets the state for the specified update. + /// + /// The update to use as a key source. + /// The state value. + public TState GetState(Update keySource) + { + TKey key = KeyResolver.ResolveKey(keySource); + return States[key]; + } + + /// + /// Tries to get the state for the specified update. + /// + /// The update to use as a key source. + /// When this method returns, contains the state value if found; otherwise, the default value. + /// True if the state was found; otherwise, false. + public bool TryGetState(Update keySource, out TState? state) + { + TKey key = KeyResolver.ResolveKey(keySource); + return States.TryGetValue(key, out state); + } + + /// + /// Determines whether a state exists for the specified update. + /// + /// The update to use as a key source. + /// True if the state exists; otherwise, false. + public bool HasState(Update keySource) + { + TKey key = KeyResolver.ResolveKey(keySource); + return States.ContainsKey(key); + } + + /// + /// Creates a state for the specified update using the default state value. + /// + /// The update to use as a key source. + public void CreateState(Update keySource) + { + TKey key = KeyResolver.ResolveKey(keySource); + States.Set(key, DefaultState); + } + + /// + /// Deletes the state for the specified update. + /// + /// The update to use as a key source. + public void DeleteState(Update keySource) + { + TKey key = KeyResolver.ResolveKey(keySource); + States.Remove(key); + } + + /// + /// Moves the state forward for the specified update. + /// + /// The update to use as a key source. + public 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; + } + + /// + /// Moves the state backward for the specified update. + /// + /// The update to use as a key source. + public void MoveBackward(Update keySource) + { + TKey key = KeyResolver.ResolveKey(keySource); + TState currentState = States[key]; + TState newState = MoveBackward(currentState, key); + States[key] = newState; + } + + /// + /// Gets the state keeper for the specified key. + /// + /// The type of the state keeper. + /// The key. + /// The state keeper instance. + protected TStateKeeper GetKeeper(TKey key) where TStateKeeper : StateKeeperBase + => States[key] as TStateKeeper ?? throw new InvalidCastException(); + + /// + /// Moves the state forward for the specified current state and key. + /// + /// The current state value. + /// The key. + /// The new state value. + protected abstract TState MoveForward(TState currentState, TKey currentKey); + + /// + /// Moves the state backward for the specified current state and key. + /// + /// The current state value. + /// The key. + /// The new state value. + protected abstract TState MoveBackward(TState currentState, TKey currentKey); + } +} diff --git a/Telegrator/StateKeeping/EnumStateKeeper.cs b/Telegrator/StateKeeping/EnumStateKeeper.cs new file mode 100644 index 0000000..81a2f88 --- /dev/null +++ b/Telegrator/StateKeeping/EnumStateKeeper.cs @@ -0,0 +1,76 @@ +using Telegrator.Annotations.StateKeeping; +using Telegrator.Handlers.Components; +using Telegrator.StateKeeping; + +namespace Telegrator.StateKeeping +{ + /// + /// State keeper implementation for enum-based states. + /// Automatically creates an array of all enum values for state navigation. + /// + /// The enum type to be used for state management. + public class EnumStateKeeper() : ArrayStateKeeper(Enum.GetValues(typeof(TEnum)).Cast().ToArray()) where TEnum : Enum + { + /// + /// Gets the default state, which is the first value in the enum. + /// + public override TEnum DefaultState => ArrayStates.ElementAt(0); + } + + /// + /// Extension methods for working with enum-based states in handler containers. + /// Provides convenient methods for state management operations. + /// + public static partial class StateHandlerContainerExtensions + { + /// + /// Gets the enum state keeper for the specified enum type. + /// + /// The enum type to get the state keeper for. + /// The handler container (unused parameter for extension method syntax). + /// The enum state keeper instance. + public static EnumStateKeeper EnumStateKeeper(this IHandlerContainer _) where TEnum : Enum + => EnumStateAttribute.StateKeeper; + + /// + /// Creates a new enum state for the current update. + /// + /// The enum type for state management. + /// The handler container. + public static void CreateEnumState(this IHandlerContainer container) where TEnum : Enum + => container.EnumStateKeeper().CreateState(container.HandlingUpdate); + + /// + /// Deletes the enum state for the current update. + /// + /// The enum type for state management. + /// The handler container. + public static void DeleteEnumState(this IHandlerContainer container) where TEnum : Enum + => container.EnumStateKeeper().DeleteState(container.HandlingUpdate); + + /// + /// Sets the enum state to a specific value for the current update. + /// + /// The enum type for state management. + /// The handler container. + /// The new state value. If null, uses the default state. + public static void SetEnumState(this IHandlerContainer container, TEnum? newState) where TEnum : Enum + => container.EnumStateKeeper().SetState(container.HandlingUpdate, newState ?? EnumStateAttribute.StateKeeper.DefaultState); + + /// + /// Moves the enum state forward to the next value in the enum sequence. + /// + /// The enum type for state management. + /// The handler container. + public static void ForwardEnumState(this IHandlerContainer container) where TEnum : Enum + => container.EnumStateKeeper().MoveForward(container.HandlingUpdate); + + /// + /// Moves the enum state backward to the previous value in the enum sequence. + /// + /// The enum type for state management. + /// The handler container. + public static void BackwardEnumState(this IHandlerContainer container) where TEnum : Enum + => container.EnumStateKeeper().MoveBackward(container.HandlingUpdate); + } +} diff --git a/Telegrator/StateKeeping/NumericStateKeeper.cs b/Telegrator/StateKeeping/NumericStateKeeper.cs new file mode 100644 index 0000000..9f398c1 --- /dev/null +++ b/Telegrator/StateKeeping/NumericStateKeeper.cs @@ -0,0 +1,92 @@ +using Telegrator.Annotations.StateKeeping; +using Telegrator.Handlers.Components; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.StateKeeping +{ + /// + /// State keeper that manages numeric (integer) states for chat sessions. + /// Inherits from with long keys and int states. + /// Provides automatic increment/decrement functionality for state transitions. + /// + public class NumericStateKeeper : StateKeeperBase + { + /// + /// Gets the default state value, which is 1. + /// + public override int DefaultState => 1; + + /// + /// Moves the numeric state backward by decrementing the current state value. + /// + /// The current numeric state value + /// The chat ID (unused in this implementation) + /// The decremented state value + protected override int MoveBackward(int currentState, long _) + { + return currentState - 1; + } + + /// + /// Moves the numeric state forward by incrementing the current state value. + /// + /// The current numeric state value + /// The chat ID (unused in this implementation) + /// The incremented state value + protected override int MoveForward(int currentState, long _) + { + return currentState + 1; + } + } + + /// + /// Provides extension methods for managing numeric states in handler containers. + /// + public static partial class StateHandlerContainerExtensions + { + /// + /// Gets the numeric state keeper instance associated with the handler container. + /// + /// The handler container instance + /// The instance + public static NumericStateKeeper NumericStateKeeper(this IHandlerContainer _) + => NumericStateAttribute.StateKeeper; + + /// + /// Creates a new numeric state for the current update being handled. + /// + /// The handler container instance + public static void CreateNumericState(this IHandlerContainer container) + => container.NumericStateKeeper().CreateState(container.HandlingUpdate); + + /// + /// Deletes the numeric state for the current update being handled. + /// + /// The handler container instance + public static void DeleteNumericState(this IHandlerContainer container) + => container.NumericStateKeeper().DeleteState(container.HandlingUpdate); + + /// + /// Sets the numeric state for the current update being handled. + /// If the new state is null, uses the default state from the state keeper. + /// + /// The handler container instance + /// The new numeric state to set, or null to use default + public static void SetNumericState(this IHandlerContainer container, int? newState) + => container.NumericStateKeeper().SetState(container.HandlingUpdate, newState ?? NumericStateAttribute.StateKeeper.DefaultState); + + /// + /// Moves the numeric state forward by incrementing the current value. + /// + /// The handler container instance + public static void ForwardNumericState(this IHandlerContainer container) + => container.NumericStateKeeper().MoveForward(container.HandlingUpdate); + + /// + /// Moves the numeric state backward by decrementing the current value. + /// + /// The handler container instance + public static void BackwardNumericState(this IHandlerContainer container) + => container.NumericStateKeeper().MoveBackward(container.HandlingUpdate); + } +} diff --git a/Telegrator/StateKeeping/SenderIdResolver.cs b/Telegrator/StateKeeping/SenderIdResolver.cs new file mode 100644 index 0000000..ebc7af6 --- /dev/null +++ b/Telegrator/StateKeeping/SenderIdResolver.cs @@ -0,0 +1,21 @@ +using Telegram.Bot.Types; +using Telegrator.StateKeeping.Components; + +namespace Telegrator.StateKeeping +{ + /// + /// 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. + /// + public class SenderIdResolver : IStateKeyResolver + { + /// + /// Resolves the sender ID from a Telegram update. + /// + /// The Telegram update to extract the sender ID from. + /// The sender ID as a long value. + /// Thrown when the update does not contain a valid sender ID. + public long ResolveKey(Update keySource) + => keySource.GetSenderId() ?? throw new ArgumentException("Cannot resolve SenderID for this Update"); + } +} diff --git a/Telegrator/StateKeeping/StringStateKeeper.cs b/Telegrator/StateKeeping/StringStateKeeper.cs new file mode 100644 index 0000000..9df77e3 --- /dev/null +++ b/Telegrator/StateKeeping/StringStateKeeper.cs @@ -0,0 +1,76 @@ +using Telegrator.Annotations.StateKeeping; +using Telegrator.Handlers.Components; +using Telegrator.StateKeeping; + +namespace Telegrator.StateKeeping +{ + /// + /// State keeper that manages string-based states for chat sessions. + /// Inherits from with long keys and string states. + /// + /// Initial array of string states to manage + public class StringStateKeeper(params string[] states) : ArrayStateKeeper(states) + { + /// + /// Initializes a new instance of with an empty state array. + /// + public StringStateKeeper() + : this([]) { } + + /// + /// Gets the default state value, which is an empty string. + /// + public override string DefaultState => string.Empty; + } + + /// + /// Provides extension methods for managing string states in handler containers. + /// + public static partial class StateHandlerContainerExtensions + { + /// + /// Gets the string state keeper instance associated with the handler container. + /// + /// The handler container instance + /// The instance + public static StringStateKeeper StringStateKeeper(this IHandlerContainer _) + => StringStateAttribute.StateKeeper; + + /// + /// Creates a new string state for the current update being handled. + /// + /// The handler container instance + public static void CreateStringState(this IHandlerContainer container) + => container.StringStateKeeper().CreateState(container.HandlingUpdate); + + /// + /// Deletes the string state for the current update being handled. + /// + /// The handler container instance + public static void DeleteStringState(this IHandlerContainer container) + => container.StringStateKeeper().DeleteState(container.HandlingUpdate); + + /// + /// Sets the string state for the current update being handled. + /// If the new state is null, uses the default state from the state keeper. + /// + /// The handler container instance + /// The new string state to set, or null to use default + public static void SetStringState(this IHandlerContainer container, string? newState) + => container.StringStateKeeper().SetState(container.HandlingUpdate, newState ?? StringStateAttribute.StateKeeper.DefaultState); + + /// + /// Moves the string state forward to the next state in the sequence. + /// + /// The handler container instance + public static void ForwardStringState(this IHandlerContainer container) + => container.StringStateKeeper().MoveForward(container.HandlingUpdate); + + /// + /// Moves the string state backward to the previous state in the sequence. + /// + /// The handler container instance + public static void BackwardStringState(this IHandlerContainer container) + => container.StringStateKeeper().MoveBackward(container.HandlingUpdate); + } +} diff --git a/Telegrator/TelegramBotInfo.cs b/Telegrator/TelegramBotInfo.cs new file mode 100644 index 0000000..eaaf633 --- /dev/null +++ b/Telegrator/TelegramBotInfo.cs @@ -0,0 +1,17 @@ +using Telegram.Bot.Types; +using Telegrator.Configuration; + +namespace Telegrator +{ + /// + /// Implementation of that provides bot information. + /// Contains metadata about the Telegram bot including user details. + /// + public class TelegramBotInfo(User user) : ITelegramBotInfo + { + /// + /// Gets the user information for the bot. + /// + public User User { get; } = user; + } +} diff --git a/Telegrator/Telegrator.csproj b/Telegrator/Telegrator.csproj new file mode 100644 index 0000000..3ef1eff --- /dev/null +++ b/Telegrator/Telegrator.csproj @@ -0,0 +1,46 @@ + + + + netstandard2.0 + enable + enable + latest + Debug;Release + True + True + + Telegrator : Telegram.Bot mediator framework + telegrator_nuget.png + README.md + https://github.com/Rikitav/Telegrator + telegram;bot;mediator;attributes;aspect;hosting;host;framework;easy;simple;handlers + True + True + LICENSE + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + + + + + + diff --git a/Telegrator/TelegratorClient.cs b/Telegrator/TelegratorClient.cs new file mode 100644 index 0000000..1e6a7e8 --- /dev/null +++ b/Telegrator/TelegratorClient.cs @@ -0,0 +1,102 @@ +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegrator.Configuration; +using Telegrator.MadiatorCore; +using Telegrator.Polling; +using Telegrator.Providers; + +namespace Telegrator +{ + /// + /// Main client class for the Telegrator library. + /// Extends TelegramBotClient with reactive capabilities for handling updates. + /// + public class TelegratorClient : TelegramBotClient, IReactiveTelegramBot, ICollectingProvider + { + /// + /// The update router for handling incoming updates. + /// + private IUpdateRouter? updateRouter = null; + + /// + public TelegramBotOptions Options { get; private set; } + + /// + public IHandlersCollection Handlers { get; private set; } + + /// + public ITelegramBotInfo BotInfo { get; private set; } + + /// + public IUpdateRouter UpdateRouter { get => updateRouter ?? throw new Exception(); } + + /// + /// Initializes a new instance of the class with a bot token. + /// + /// The bot token from BotFather. + /// Optional HTTP client for making requests. + /// The cancellation token. + public TelegratorClient(string token, HttpClient? httpClient = null, CancellationToken cancellationToken = default) + : this(new TelegramBotClientOptions(token), httpClient, cancellationToken) { } + + /// + /// Initializes a new instance of the class with bot options. + /// + /// The Telegram bot client options. + /// Optional HTTP client for making requests. + /// The cancellation token. + public TelegratorClient(TelegramBotClientOptions options, HttpClient? httpClient = null, CancellationToken cancellationToken = default) : base(options, httpClient, cancellationToken) + { + Options = new TelegramBotOptions(); + Handlers = new HandlersCollection(default); + BotInfo = new TelegramBotInfo(this.GetMe(cancellationToken).Result); + } + + /// + /// Starts receiving updates from Telegram. + /// Initializes the update router and begins polling for updates. + /// + /// Optional receiver options for configuring update polling. + /// The cancellation token to stop receiving updates. + public void StartReceiving(ReceiverOptions? receiverOptions = null, CancellationToken cancellationToken = default) + { + if (Options.GlobalCancellationToken == CancellationToken.None) + Options.GlobalCancellationToken = cancellationToken; + + HandlersProvider handlerProvider = new HandlersProvider(Handlers, Options, BotInfo); + AwaitingProvider awaitingProvider = new AwaitingProvider(Options, BotInfo); + + updateRouter = new UpdateRouter(handlerProvider, awaitingProvider, Options); + StartReceivingInternal(receiverOptions, cancellationToken); + } + + /// + /// Internal method that starts the update receiving process. + /// Handles the reactive update receiver and error handling. + /// + /// Optional receiver options for configuring update polling. + /// The cancellation token to stop receiving updates. + private async void StartReceivingInternal(ReceiverOptions? receiverOptions, CancellationToken cancellationToken) + { + try + { + try + { + await new ReactiveUpdateReceiver(this, receiverOptions) + .ReceiveAsync(UpdateRouter, cancellationToken) + .ConfigureAwait(false); + } + catch (Exception exception) + { + await UpdateRouter + .HandleErrorAsync(this, exception, HandleErrorSource.FatalError, cancellationToken) + .ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + // Cancelled + } + } + } +} diff --git a/Telegrator/TypesExtensions.cs b/Telegrator/TypesExtensions.cs new file mode 100644 index 0000000..2e7abd7 --- /dev/null +++ b/Telegrator/TypesExtensions.cs @@ -0,0 +1,905 @@ +using System.Collections; +using System.Collections.ObjectModel; +using System.Reflection; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.Payments; +using Telegrator.Annotations.StateKeeping; +using Telegrator.Attributes; +using Telegrator.Filters.Components; +using Telegrator.Handlers.Building; +using Telegrator.Handlers.Building.Components; +using Telegrator.Handlers.Components; +using Telegrator.MadiatorCore; +using Telegrator.Providers; +using Telegrator.StateKeeping; +using Telegrator.StateKeeping.Components; + +namespace Telegrator +{ + /// + /// Provides usefull helper methods for messages + /// + public static class MessageExtensions + { + /// + /// Substrings entity content from text + /// + /// + /// + /// + /// + public static string SubstringEntity(this Message message, MessageEntity entity) + { + if (message.Text == null || string.IsNullOrEmpty(message.Text)) // DO NOT CHANGE! Compiler SOMEHOW warnings "probably null" here + throw new ArgumentNullException(nameof(message), "Cannot substring entity from message with text that null or empty"); + + return message.Text.Substring(entity.Offset, entity.Length); + } + } + + /// + /// Extension methods for handler containers. + /// Provides convenient methods for creating awaiter builders and state keeping. + /// + public static class HandlerContainerExtensions + { + /// + /// Creates an awaiter builder for a specific update type. + /// + /// The type of update to await. + /// The handler container. + /// The type of update to await. + /// An awaiter builder for the specified update type. + public static IAwaiterHandlerBuilder AwaitUpdate(this IHandlerContainer container, UpdateType updateType) where TUpdate : class + => container.AwaitingProvider.CreateAbstract(updateType, container.HandlingUpdate); + + /// + /// Creates an awaiter builder for any update type. + /// + /// The handler container. + /// An awaiter builder for any update type. + public static IAwaiterHandlerBuilder AwaitAny(this IHandlerContainer container) + => container.AwaitUpdate(UpdateType.Unknown); + + /// + /// Creates an awaiter builder for message updates. + /// + /// The handler container. + /// An awaiter builder for message updates. + public static IAwaiterHandlerBuilder AwaitMessage(this IHandlerContainer container) + => container.AwaitUpdate(UpdateType.Message); + + /// + /// Creates an awaiter builder for callback query updates. + /// + /// The handler container. + /// An awaiter builder for callback query updates. + public static IAwaiterHandlerBuilder AwaitCallbackQuery(this IHandlerContainer container) + => container.AwaitUpdate(UpdateType.CallbackQuery); + + /// + /// Gets a state keeper instance for the specified types. + /// + /// The type of the state key. + /// The type of the state value. + /// The type of the state keeper. + /// The handler container (unused). + /// The state keeper instance. + public static TKeeper GetStateKeeper(this IHandlerContainer _) where TKey : notnull where TState : IEquatable where TKeeper : StateKeeperBase, new() + => StateKeeperAttribute.StateKeeper; + } + + /// + /// Extensions methods for Awaiter Handler Builders + /// + public static class AwaiterHandlerBuilderExtensions + { + /// + /// Awaits an update using the chat id key resolver and cancellation token. + /// + /// + /// + /// + /// + public static async Task ByChatId(this IAwaiterHandlerBuilder builder, CancellationToken cancellationToken = default) where TUpdate : class + => await builder.Await(new ChatIdResolver(), cancellationToken); + + /// + /// Awaits an update using the sender id key resolver and cancellation token. + /// + /// + /// + /// + /// + public static async Task BySenderId(this IAwaiterHandlerBuilder builder, CancellationToken cancellationToken = default) where TUpdate : class + => await builder.Await(new SenderIdResolver(), cancellationToken); + } + + /// + /// Extensions methods for awaiting providers + /// Provides convenient methods for creating awaiter builders. + /// + public static class AwaitingProviderExtensions + { + /// + /// Creates an awaiter handler builder for a specific update type. + /// + /// The type of update to await. + /// + /// The type of update to await. + /// The update that triggered the awaiter creation. + /// An awaiter handler builder for the specified update type. + public static IAwaiterHandlerBuilder CreateAbstract(this IAwaitingProvider awaitingProvider, UpdateType updateType, Update handlingUpdate) where TUpdate : class + => new AwaiterHandlerBuilder(updateType, handlingUpdate, awaitingProvider); + + /// + /// Creates an awaiter builder for any update type. + /// + /// + /// + /// An awaiter builder for any update type. + public static IAwaiterHandlerBuilder AwaitAny(this IAwaitingProvider awaitingProvider, Update handlingUpdate) + => awaitingProvider.CreateAbstract(UpdateType.Unknown, handlingUpdate); + + /// + /// Creates an awaiter builder for message updates. + /// + /// + /// + /// An awaiter builder for message updates. + public static IAwaiterHandlerBuilder AwaitMessage(this IAwaitingProvider awaitingProvider, Update handlingUpdate) + => awaitingProvider.CreateAbstract(UpdateType.Message, handlingUpdate); + + /// + /// Creates an awaiter builder for callback query updates. + /// + /// + /// + /// An awaiter builder for callback query updates. + public static IAwaiterHandlerBuilder AwaitCallbackQuery(this IAwaitingProvider awaitingProvider, Update handlingUpdate) + => awaitingProvider.CreateAbstract(UpdateType.CallbackQuery, handlingUpdate); + } + + /// + /// Extension methods for handlers collections. + /// Provides convenient methods for creating implicit handlers. + /// + public static class HandlersCollectionExtensions + { + /// + /// Creates a handler builder for a specific update type. + /// + /// The type of update to handle. + /// The handlers collection. + /// The type of update to handle. + /// A handler builder for the specified update type. + public static HandlerBuilder CreateHandler(this IHandlersCollection handlers, UpdateType updateType) where TUpdate : class + => new HandlerBuilder(updateType, handlers); + + /// + /// Creates a handler builder for any update type. + /// + /// The handlers collection. + /// A handler builder for any update type. + public static HandlerBuilder CreateAny(this IHandlersCollection handlers) + => handlers.CreateHandler(UpdateType.Unknown); + + /// + /// Creates a handler builder for message updates. + /// + /// The handlers collection. + /// A handler builder for message updates. + public static HandlerBuilder CreateMessage(this IHandlersCollection handlers) + => handlers.CreateHandler(UpdateType.Message); + + /// + /// Creates a handler builder for callback query updates. + /// + /// The handlers collection. + /// A handler builder for callback query updates. + public static HandlerBuilder CreateCallbackQuery(this IHandlersCollection handlers) + => handlers.CreateHandler(UpdateType.CallbackQuery); + + /* + public static IHandlersCollection AddMethod(this IHandlersCollection handlers, Func, CancellationToken, Task> method) + { + + } + */ + } + + /// + /// Extension methods for handler builders. + /// Provides convenient methods for creating handlers and setting state keepers. + /// + public static partial class HandlerBuilderExtensions + { + /// + public static TBuilder SetUpdateValidating(this TBuilder handlerBuilder, UpdateValidateAction updateValidateAction) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.SetUpdateValidating(updateValidateAction); + return handlerBuilder; + } + + /// + public static TBuilder SetConcurreny(this TBuilder handlerBuilder, int concurrency) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.SetConcurreny(concurrency); + return handlerBuilder; + } + + /// + public static TBuilder SetPriority(this TBuilder handlerBuilder, int priority) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.SetPriority(priority); + return handlerBuilder; + } + + /// + public static TBuilder SetIndexer(this TBuilder handlerBuilder, int concurrency, int priority) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.SetIndexer(concurrency, priority); + return handlerBuilder; + } + + /// + public static TBuilder AddFilter(this TBuilder handlerBuilder, IFilter filter) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.AddFilter(filter); + return handlerBuilder; + } + + /// + public static TBuilder AddFilters(this TBuilder handlerBuilder, params IFilter[] filters) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.AddFilters(filters); + return handlerBuilder; + } + + /// + public static TBuilder SetStateKeeper(this TBuilder handlerBuilder, TState myState, IStateKeyResolver keyResolver) + where TBuilder : HandlerBuilderBase + where TKey : notnull + where TState : IEquatable + where TKeeper : StateKeeperBase, new() + { + handlerBuilder.SetStateKeeper(myState, keyResolver); + return handlerBuilder; + } + + /// + public static TBuilder SetStateKeeper(this TBuilder handlerBuilder, SpecialState specialState, IStateKeyResolver keyResolver) + where TBuilder : HandlerBuilderBase + where TKey : notnull + where TState : IEquatable + where TKeeper : StateKeeperBase, new() + { + handlerBuilder.SetStateKeeper(specialState, keyResolver); + return handlerBuilder; + } + + /// + /// Adds a targeted filter for a specific filter target type. + /// + /// + /// The type of the filter target. + /// + /// Function to get the filter target from an update. + /// The filter to add. + /// The builder instance. + public static TBuilder AddTargetedFilter(this TBuilder handlerBuilder, Func getFilterringTarget, IFilter filter) + where TBuilder : HandlerBuilderBase + where TFilterTarget : class + { + handlerBuilder.AddTargetedFilter(getFilterringTarget, filter); + return handlerBuilder; + } + + /// + /// Adds multiple targeted filters for a specific filter target type. + /// + /// + /// The type of the filter target. + /// + /// Function to get the filter target from an update. + /// The filters to add. + /// The builder instance. + public static TBuilder AddTargetedFilters(this TBuilder handlerBuilder, Func getFilterringTarget, params IFilter[] filters) + where TBuilder : HandlerBuilderBase + where TFilterTarget : class + { + handlerBuilder.AddTargetedFilters(getFilterringTarget, filters); + return handlerBuilder; + } + + /// + /// Sets a numeric state keeper with a custom key resolver. + /// + /// The type of the handler builder. + /// The handler builder. + /// The numeric state value. + /// The key resolver for the state. + /// The handler builder for method chaining. + public static TBuilder SetNumericState(this TBuilder handlerBuilder, int myState, IStateKeyResolver keyResolver) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.SetStateKeeper(myState, keyResolver); + return handlerBuilder; + } + + /// + /// Sets a numeric state keeper with a special state and custom key resolver. + /// + /// The type of the handler builder. + /// The handler builder. + /// The special state value. + /// The key resolver for the state. + /// The handler builder for method chaining. + public static TBuilder SetNumericState(this TBuilder handlerBuilder, SpecialState specialState, IStateKeyResolver keyResolver) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.SetStateKeeper(specialState, keyResolver); + return handlerBuilder; + } + + /// + /// Sets a numeric state keeper with the default sender ID resolver. + /// + /// The type of the handler builder. + /// The handler builder. + /// The numeric state value. + /// The handler builder for method chaining. + public static TBuilder SetNumericState(this TBuilder handlerBuilder, int myState) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.SetStateKeeper(myState, new SenderIdResolver()); + return handlerBuilder; + } + + /// + /// Sets a numeric state keeper with a special state and the default sender ID resolver. + /// + /// The type of the handler builder. + /// The handler builder. + /// The special state value. + /// The handler builder for method chaining. + public static TBuilder SetNumericState(this TBuilder handlerBuilder, SpecialState specialState) + where TBuilder : HandlerBuilderBase + { + handlerBuilder.SetStateKeeper(specialState, new SenderIdResolver()); + return handlerBuilder; + } + + /// + /// Sets an enum state keeper with a custom key resolver. + /// + /// The type of the handler builder. + /// The type of the enum state. + /// The handler builder. + /// The enum state value. + /// The key resolver for the state. + /// The handler builder for method chaining. + public static TBuilder SetEnumState(this TBuilder handlerBuilder, TEnum myState, IStateKeyResolver keyResolver) + where TBuilder : HandlerBuilderBase + where TEnum : Enum, IEquatable + { + handlerBuilder.SetStateKeeper>(myState, keyResolver); + return handlerBuilder; + } + + /// + /// Sets an enum state keeper with a special state and custom key resolver. + /// + /// The type of the handler builder. + /// The type of the enum state. + /// The handler builder. + /// The special state value. + /// The key resolver for the state. + /// The handler builder for method chaining. + public static TBuilder SetEnumState(this TBuilder handlerBuilder, SpecialState specialState, IStateKeyResolver keyResolver) + where TBuilder : HandlerBuilderBase + where TEnum : Enum, IEquatable + { + handlerBuilder.SetStateKeeper>(specialState, keyResolver); + return handlerBuilder; + } + + /// + /// Sets an enum state keeper with the default sender ID resolver. + /// + /// The type of the handler builder. + /// The type of the enum state. + /// The handler builder. + /// The enum state value. + /// The handler builder for method chaining. + public static TBuilder SetEnumState(this TBuilder handlerBuilder, TEnum myState) + where TBuilder : HandlerBuilderBase + where TEnum : Enum, IEquatable + { + handlerBuilder.SetStateKeeper>(myState, new SenderIdResolver()); + return handlerBuilder; + } + + /// + /// Sets an enum state keeper with a special state and the default sender ID resolver. + /// + /// The type of the handler builder. + /// The type of the enum state. + /// The handler builder. + /// The special state value. + /// The handler builder for method chaining. + public static TBuilder SetEnumState(this TBuilder handlerBuilder, SpecialState specialState) + where TBuilder : HandlerBuilderBase + where TEnum : Enum, IEquatable + { + handlerBuilder.SetStateKeeper>(specialState, new SenderIdResolver()); + return handlerBuilder; + } + } + + /// + /// Provides extension methods for working with collections. + /// + public static partial class ColletionsExtensions + { + /// + /// Creates a from an + /// according to a specified key selector function. + /// + /// + /// + /// + /// + /// + public static ReadOnlyDictionary ToReadOnlyDictionary(this IEnumerable source, Func keySelector) where TKey : notnull + { + Dictionary dictionary = source.ToDictionary(keySelector); + return new ReadOnlyDictionary(dictionary); + } + + /// + /// Enumerates objects in a and executes an on each one + /// + /// + /// + /// + /// + public static IEnumerable ForEach(this IEnumerable source, Action action) + { + foreach (TValue value in source) + action.Invoke(value); + + return source; + } + + /// + /// Creates a new with the elements of the that were successfully cast to the + /// + /// + /// + /// + public static IEnumerable WhereCast(this IEnumerable source) + { + foreach (object value in source) + { + if (value is TResult result) + yield return result; + } + } + + /// + /// Sets the value of a key in a dictionary, or if the key does not exist, adds it + /// + /// + /// + /// + /// + /// + public static void Set(this IDictionary source, TKey key, TValue value) + { + if (source.ContainsKey(key)) + source[key] = value; + else + source.Add(key, value); + } + + /// + /// Sets the value of a key in a dictionary, or if the key does not exist, adds its default value. + /// + /// + /// + /// + /// + /// + /// + public static void Set(this IDictionary source, TKey key, TValue value, TValue defaultValue) + { + if (source.ContainsKey(key)) + source[key] = value; + else + source.Add(key, defaultValue); + } + + /// + /// Return the random object from + /// + /// + /// + /// + public static TSource Random(this IEnumerable source) + => source.Random(new Random()); + + /// + /// Return the random object from + /// + /// + /// + /// + /// + public static TSource Random(this IEnumerable source, Random random) + => source.ElementAt(random.Next(0, source.Count() - 1)); + + /// + /// Adds a range of elements to collection if they dont already exist using default equality comparer + /// + /// + /// + /// + public static void Union(this IList list, params IEnumerable elements) + { + foreach (TSource item in elements) + { + if (!list.Contains(item, EqualityComparer.Default)) + list.Add(item); + } + } + } + + /// + /// Provides extension methods for reflection and type inspection. + /// + public static partial class ReflectionExtensions + { + private static readonly BindingFlags BindAll = BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; + + /// + /// Checks if a type implements the interface. + /// + /// The type to check. + /// True if the type implements ICustomDescriptorsProvider; otherwise, false. + public static bool IsCustomDescriptorsProvider(this Type type) + => type.GetInterface(nameof(ICustomDescriptorsProvider)) != null; + + /// + /// Checks if is a + /// + /// + /// + public static bool IsFilterType(this Type type) + => type.IsAssignableToGenericType(typeof(IFilter<>)); + + /// + /// Checks if is a descendant of class + /// + /// + /// + public static bool IsHandlerAbstract(this Type type) + => type.IsAbstract && typeof(UpdateHandlerBase).IsAssignableFrom(type); + + /// + /// Checks if is an implementation of class or its descendants + /// + /// + /// + public static bool IsHandlerRealization(this Type type) + => !type.IsAbstract && type != typeof(UpdateHandlerBase) && typeof(UpdateHandlerBase).IsAssignableFrom(type); + + /// + /// Checks if has a parameterless constructor + /// + /// + /// + public static bool HasParameterlessCtor(this Type type) + => type.GetConstructors().Any(ctor => ctor.GetParameters().Length == 0); + + /// + /// Invokes a "" method of an object + /// + /// + /// + /// + /// + public static object? InvokeMethod(this object obj, string methodName, params object[]? args) + => obj.GetType().GetMethod(methodName, BindAll).InvokeMethod(obj, args); + + /// + /// Invokes a method of + /// + /// + /// + /// + /// + public static object? InvokeMethod(this MethodInfo methodInfo, object obj, params object[]? args) + => methodInfo.Invoke(obj, args); + + /// + /// Invokes a static method of + /// + /// + /// + /// + public static object? InvokeStaticMethod(this MethodInfo methodInfo, params object[]? parameters) + => methodInfo.Invoke(null, parameters); + + /// + /// Invokes a static "" method of an object + /// + /// + /// + /// + /// + /// + public static T? InvokeStaticMethod(this object obj, string methodName, params object[]? parameters) + => (T?)obj.GetType().GetMethod(methodName, BindAll).InvokeStaticMethod(parameters); + + /// + /// Invokes a generic method of with generic types in + /// + /// + /// + /// + /// + /// + public static object InvokeGenericMethod(this MethodInfo methodInfo, object obj, Type[] genericParameters, params object[]? parameters) + => methodInfo.MakeGenericMethod(genericParameters).Invoke(obj, parameters); + + /// + /// Invokes a generic method with generic types in + /// + /// + /// + /// + /// + /// + /// + public static T? InvokeGenericMethod(this object obj, string methodName, Type[] genericParameters, params object[]? parameters) + => (T?)obj.GetType().GetMethod(methodName).InvokeGenericMethod(obj, genericParameters, parameters); + + /// + /// Checks is has public properties + /// + /// + /// + public static bool HasPublicProperties(this object obj) + => obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(prop => prop.Name != "IsCollectible").Any(); + + /// + /// Determines whether an instance of a specified type can be assigned to an instance of the current type + /// + /// + /// + /// + public static bool IsAssignableToGenericType(this Type givenType, Type genericType) + { + if (givenType.GetInterfaces().Any(inter => inter.IsGenericType && inter.GetGenericTypeDefinition() == genericType)) + return true; + + if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + return true; + + if (givenType.BaseType == null) + return false; + + return givenType.BaseType.IsAssignableToGenericType(genericType); + } + } + + /// + /// Provides extension methods for string manipulation. + /// + public static partial class StringExtensions + { + /// + /// Slices a string into a array of substrings of fixed + /// + /// + /// + /// + public static IEnumerable SliceBy(this string source, int length) + { + for (int start = 0; start < source.Length; start += length + 1) + { + int tillEnd = source.Length - start; + int toSlice = tillEnd < length + 1 ? tillEnd : length + 1; + + ReadOnlySpan chunk = source.AsSpan().Slice(start, toSlice); + yield return chunk.ToString(); + } + } + } + + /// + /// Provides extension methods for working with Telegram Update objects. + /// + public static partial class UpdateExtensions + { + /// + /// Selects from Update an object from which you can get the sender's ID + /// + /// + /// Sender's ID + public static long? GetSenderId(this Update update) => update switch + { + { Message.From: { } from } => from.Id, + { Message.SenderChat: { } chat } => chat.Id, + { EditedMessage.From: { } from } => from.Id, + { EditedMessage.SenderChat: { } chat } => chat.Id, + { ChannelPost.From: { } from } => from.Id, + { ChannelPost.SenderChat: { } chat } => chat.Id, + { EditedChannelPost.From: { } from } => from.Id, + { EditedChannelPost.SenderChat: { } chat } => chat.Id, + { CallbackQuery.From: { } from } => from.Id, + { InlineQuery.From: { } from } => from.Id, + { PollAnswer.User: { } user } => user.Id, + { PreCheckoutQuery.From: { } from } => from.Id, + { ShippingQuery.From: { } from } => from.Id, + { ChosenInlineResult.From: { } from } => from.Id, + { ChatJoinRequest.From: { } from } => from.Id, + { ChatMember.From: { } from } => from.Id, + { MyChatMember.From: { } from } => from.Id, + _ => null + }; + + /// + /// Selects from Update an object from which you can get the chat's ID + /// + /// + /// Sender's ID + public static long? GetChatId(this Update update) => update switch + { + { Message.Chat: { } chat } => chat.Id, + { Message.SenderChat: { } chat } => chat.Id, + { EditedMessage.Chat: { } chat } => chat.Id, + { EditedMessage.SenderChat: { } chat } => chat.Id, + { ChannelPost.Chat: { } chat } => chat.Id, + { ChannelPost.SenderChat: { } chat } => chat.Id, + { EditedChannelPost.Chat: { } chat } => chat.Id, + { EditedChannelPost.SenderChat: { } chat } => chat.Id, + { CallbackQuery.Message.Chat: { } chat } => chat.Id, + { ChatJoinRequest.Chat: { } chat } => chat.Id, + { ChatMember.Chat: { } chat } => chat.Id, + { MyChatMember.Chat: { } chat } => chat.Id, + _ => null + }; + + /// + /// Selects from an object that contains information about the update + /// + /// + /// + public static object GetActualUpdateObject(this Update update) => update switch + { + { Message: { } message } => message, + { EditedMessage: { } editedMessage } => editedMessage, + { ChannelPost: { } channelPost } => channelPost, + { EditedChannelPost: { } editedChannelPost } => editedChannelPost, + { BusinessConnection: { } businessConnection } => businessConnection, + { BusinessMessage: { } businessMessage } => businessMessage, + { EditedBusinessMessage: { } editedBusinessMessage } => editedBusinessMessage, + { DeletedBusinessMessages: { } deletedBusinessMessages } => deletedBusinessMessages, + { MessageReaction: { } messageReaction } => messageReaction, + { MessageReactionCount: { } messageReactionCount } => messageReactionCount, + { InlineQuery: { } inlineQuery } => inlineQuery, + { ChosenInlineResult: { } chosenInlineResult } => chosenInlineResult, + { CallbackQuery: { } callbackQuery } => callbackQuery, + { ShippingQuery: { } shippingQuery } => shippingQuery, + { PreCheckoutQuery: { } preCheckoutQuery } => preCheckoutQuery, + { PurchasedPaidMedia: { } purchasedPaidMedia } => purchasedPaidMedia, + { Poll: { } poll } => poll, + { PollAnswer: { } pollAnswer } => pollAnswer, + { MyChatMember: { } myChatMember } => myChatMember, + { ChatMember: { } chatMember } => chatMember, + { ChatJoinRequest: { } chatJoinRequest } => chatJoinRequest, + { ChatBoost: { } chatBoost } => chatBoost, + { RemovedChatBoost: { } removedChatBoost } => removedChatBoost, + _ => update + }; + + /// + /// Selects from an that contains information about the update + /// + /// + /// + public static T GetActualUpdateObject(this Update update) + { + object actualUpdate = update.GetActualUpdateObject() ?? throw new Exception(); + if (actualUpdate is not T actualCasted) + throw new Exception(); + + return actualCasted; + } + } + + /// + /// Provides extension methods for working with UpdateType enums. + /// + public static partial class UpdateTypeExtensions + { + /// + /// 's that contain a message + /// + public static readonly UpdateType[] MessageTypes = + [ + UpdateType.Message, + UpdateType.EditedMessage, + UpdateType.BusinessMessage, + UpdateType.EditedBusinessMessage, + UpdateType.ChannelPost, + UpdateType.EditedChannelPost + ]; + + /// + /// Checks if matches one of the 's give on + /// + /// + /// + /// + public static bool IsUpdateObjectAllowed(this UpdateType[] allowedTypes) where T : class + { + return allowedTypes.Any(t => t.IsValidUpdateObject()); + } + + /// + /// Checks if matches the given + /// + /// + /// + /// + public static bool IsValidUpdateObject(this UpdateType updateType) where TUpdate : class + { + if (typeof(TUpdate) == typeof(Update)) + return true; + + return typeof(TUpdate).Equals(updateType.ReflectUpdateObject()); + } + + /// + /// Returns an update object corresponding to the . + /// + /// + /// + public static Type? ReflectUpdateObject(this UpdateType updateType) + { + return updateType switch + { + UpdateType.Message or UpdateType.EditedMessage or UpdateType.BusinessMessage or UpdateType.EditedBusinessMessage or UpdateType.ChannelPost or UpdateType.EditedChannelPost => typeof(Message), + UpdateType.MyChatMember => typeof(ChatMemberUpdated), + UpdateType.ChatMember => typeof(ChatMemberUpdated), + UpdateType.InlineQuery => typeof(InlineQuery), + UpdateType.ChosenInlineResult => typeof(ChosenInlineResult), + UpdateType.CallbackQuery => typeof(CallbackQuery), + UpdateType.ShippingQuery => typeof(ShippingQuery), + UpdateType.PreCheckoutQuery => typeof(PreCheckoutQuery), + UpdateType.Poll => typeof(Poll), + UpdateType.PollAnswer => typeof(PollAnswer), + UpdateType.ChatJoinRequest => typeof(ChatJoinRequest), + UpdateType.MessageReaction => typeof(MessageReactionUpdated), + UpdateType.MessageReactionCount => typeof(MessageReactionCountUpdated), + UpdateType.ChatBoost => typeof(ChatBoostUpdated), + UpdateType.RemovedChatBoost => typeof(ChatBoostRemoved), + UpdateType.BusinessConnection => typeof(BusinessConnection), + UpdateType.DeletedBusinessMessages => typeof(BusinessMessagesDeleted), + UpdateType.PurchasedPaidMedia => typeof(PaidMediaPurchased), + _ or UpdateType.Unknown => typeof(Update) + }; + } + } + +} diff --git a/resources/Telegrator_logo.ai b/resources/Telegrator_logo.ai new file mode 100644 index 0000000..bdaafd2 --- /dev/null +++ b/resources/Telegrator_logo.ai @@ -0,0 +1,643 @@ +%PDF-1.6 % +1 0 obj <>/OCGs[19 0 R 20 0 R 21 0 R 22 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + Adobe Illustrator 27.0 (Windows) + 2025-07-24T22:38:16+05:00 + 2025-07-24T22:38:16+04:00 + 2025-07-24T22:38:16+04:00 + + + + 256 + 240 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgA8AEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYqtlljijaWRgqICzMewGLDJMQiZS2AeeeY/Ps5laG0LRoNgqHix92YdPkMl GBLwfaPtBkyEjGeGHlz+JYjL5rIm4yyQiQ/ss3xn72yfhh0RnOW9WnGmecLmBgFleD2ryj+kH+mR OMuVpu0s2I+mRH3fJnGj+b7e5Cx3nGJm+zOp/dt8/wCXIPW9n+0MMnpy+k9/T9n3Lp/P3laGb0jd lyCQzojsop703+iuS4C5eT2g0kZVxX7ga/HuTiw1Kw1CAT2U6Txd2Q1ofAjqD88BFOz0+px5o8WO QkETgb3Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqhr/U9P0+MSXtwkCMa KXNKn2HfCBbRqNVjwi8khEea2x1fS77/AHjuopyNyqMCw+a9cSCGODWYcv0SjL3FS1nX9M0e39a8 loT/AHcK7yP/AKq/x6YiJLXre0MWmjxZD8Opeca55/1PUi8USC2sz0iG7MexdvxoMtGMPDdo9vZd QDEenH3dT7ywufSdb1zUrXSbG5jsY7tiJ7+UnYnoihfiq3+ZGSlsHD7NwwyZRGR3kaF/j5MT/MDy RF5Z8yW+g2l1JqF1JDE00rII6zTMwCotW2px6nIAvQ6vSjFMQBs0m+teTvzA8hN61xF9e0dT8U8J aSAD/KqA8R9yKfPESaNf2PKIuQ/zh+lOfL3meG7i9SzkrT+9tn6r8x/EZMxEnm8mKeI0U3uBa3o9 WD93c/txHbl8vfIi48+TWaPJT0rVNR0u8W4spDHKNivUMP5WXuMmRbdpNXkwT48Zo/f73qGh+drG /jVLxGsbr9oSAiMnxVzsP9llMoEPd6D2gw5hU/RPz5fP9bIopopUDxOsiHoykMPvGQd5DJGYuJBH kuxZuxV2KuxVB6nqltp8HqS/E7bRxDqx/p74QLcHX9oY9NDilzPId7F5PP0glIBtlWv2GJJHsTyH 6ss8N5eXtRlvaMK+P6/0I1/PNsluJXgoa71kASnajU/hg8NzD7UQ4b4Dfv2+dfoR+i+ZrHVG9NKR zHdV5BlanXiwpuB2pkZRp2XZ3bOPUnhrhn3d/uKcZF3DsVdirsVdirsVdirsVdiriQASTQDck4qS xZ/zI8urctDScorFfXCKYzTuPi5U/wBjlnhF54+02lE+H1V31t99/YmR83+W/qrXI1CIoorwrSQ+ wjNH/DI8Bc7+WNLwcXiRr7flzeVeYtcuNZ1KS7kqsf2YIv5EHQfPucyIxoPn3aWvlqspmeXQdwSz n6fx8uHHflWlPpyVOCLvZsztPSUyGXkKiQnlUfPGkzMifVzapjTFBaxpzahp8lqsphZ6FXHipqK+ 2Ahtw5OCQlVsPdvNul65p+q3kLalLpbR/VXl5zR8YG5RqSpDcVPStMrMXoMPaETISuzHvejJ/wA5 G6pJD6L+WVlmbZ6TvwYEUI4GJj+OV8DvP5d23iPmwa/s9T1zV11LTtHh8tjcv6LSICT34MdvkqgZ ZGJef1uuwzP0j3D8Uyu1imjt40ml9eVRR5eIXkfGg2GW06CRBOwoKuNMURDf3kP93KwH8p3H3HBT ISIR0PmO9iYNxXkOjLVW+8HBwtkM8omwaKPj886ohqXlr7ys34EYPDDmw7Y1Mf45fO/vRQ/MXVO8 jj/YxH9a4PCDkj2h1X877I/qXn8xtS7M3/AR/wBMfCDP/RHqv532BDzfmJrrKVR6V7kJ/wAaqp/H Hww1S9oNWRXF9kf1JTqev31+qiWRmPECRyTU+3suSEadbn1OTKbmST5sN8z2Xmeb0ZdEuxCY6+pA 3Ecj2ILBh9ByYbNJPCLGQWmHlrVddk0+S31a1ME6/BItQY3qNnTiSAR3wEMNRGET6DcSnGl381hf wXUbFfSkV2APUKakHARbHS5ziyRmP4SC91zEfXHYq7FXYq7FXYq7FVG8vbSyga4u5VhhXq7mgqe3 zwgW1Zs8MUeKZEYoC381eXbg0j1CGvg7en/xPjkjA9zh4+1tLPlkj8dvvY95881W6WH6O0+dZZrk UnkjYMFj7rUd2/Vk8ePey6b2g7WiMfhYpAylzroP2/c84pmRTwzqY0qGvr6Cyi9SU7nZEHVjjTZj xmZoMbW6u9a1BbUuVhHxSKnRVH6zkqpzzCOKN9WVxxpGixoKIgCqPADI060mzbUjxxRtJIwSNRVn YgAAdyTjSgEmgla+avLzzCFb6MuTQU5Ur/rUph4S5B0eUC+FNhuKjcHocFOM3TGldTGldTGldTGl dTGldTGldTGldTGldTGlaYhQWY0UbknYY0mksufM/l+2JEt9FUdQh9Qj/gOWHhLkR0mWXKJQy+dv K5NBe9fGOUfrTHhLP8hm/m/aEda65o11QQXsLsei8wG/4E0OPC0z0+SPOJTK2ge4uIYIt5J3WOMe JY0FMBY4sRnIRHMmnvWYL6+7FXYq7FXYq7FXYq82/MnVjPqEWmxt+6tRzlA7yONv+BX9eZWGO1vC +0+s48oxDlDc+8/qH3sNpl1PLupjSupjSqdzPHbwPNIaIgqf6Y0mMTI0GDatqM07vcSH4j8Ma9lH YZMB2+HEBsE28k24ENxOd2YqK/eT/DAXG18twGT0yNOAkOvaDeazeRQSzmHSYlDSKh+OSQk7eFFF OuSGzmafURxRJAuf3KeqeQ9W8oWdv5p0QHU9DmjpfxlQZbcgkMGIFeFRs4/2Q8YDICaPN389CdRp o5Lux06H3Jho2v6Zq8PO0l/eAVeBtpF+Y/iMlTzefTTxn1BMqYKaHUxpXUxpUHqGr6Zp6cry5SHu FJ+I/JRVj92GmzHhnP6RbF9Q/MqyjJWxtXnP+/JD6a/MAcifww8LscfZcj9RpKh+YfmKeZY4IIS7 kLHEiOzMTsAByJJw8IcqPZePzZJHd/mbFCJrryndvF3KW1whp4kFXI+kZC497OfYMq2Evkgm/Mi3 iZo7nTpoZk2eMsKg+B5BT+GS4XDl2TIHmpR+edZ1W6Sw0HSmnvJTSNAGmc+JCIB08a0xIA5t2Hsj iNEk+5lWl/kX+YPmApP5m1JNNtmoTbV9aUf88oysS/8ABV9splniOT0Ol7E4egj9pW/mZ+RFp5e8 sjV9BmuLtrL4tTScqxMR6yoEVacD9ob/AA79sceezRcjV9nCEOKNmuaW/kv+U1t5qM+r65G/6FhP pQRKWjM8v7RDCh4INjTv8jhzZeHYc2vQaMZPVL6WVfm9+T/kvRvJd3reiWr2N3YtESiyyypKssqR EMJWkIpzqKZDFmkZUXJ1uixwxmURRCJ/Ij8q59LC+adcg9O+lSmmWrj4okcbyuD0dxso7D57DPlv YMuz9Hw+uQ36PacxnbOxV2KuxV2KsT86+a7zSJre1sggmkUyyO45UWvFQB7kHL8OIS3Lzfbva89N KMMdcRFm2Lt+YPmUqQJY1J7iMVH31y/wIvPn2k1fePkx2eWaeZ5pnLyyMWd23JJ3Jy0CnSTnKcjK RslZTGmDqY0rqY0rG/Mt+sjpaRtVU+KQjpy7D6MNOdpcdeosX1D7KD3OLsMbJ/JUga2nj7gq1PmC P4Y04GuG4LJaYKcB1MaVlvkTzHFYTPp16wFlcmqu32UkIpv/AJLDY5Rmx3uHpPZ/tQYZeFM+iX2H 9RQPnb/nH/R9TnbUvLM40XUSS5hFfqzMe68fii/2NR/k5XDUEc3qtT2ZCe8dvueeXvln86PL7enN pkmqQrskkKfWwR4/uT6v/BDMgZYnq89n7B3+k/5qDTWfzHkbhF5YuWc/ZUWl0T9wGS4o97iDsTyn 8v2JpZeRvzp19gptho9s2xkmZbcD6Pjn+4ZA5ohzsHYH9H/Tfj9DN/K//OOmgWcq3fmO8k1i6qGa BeUUFf8AKNTI/wB4+WUS1BPJ3uDsqEfq3+56TpemeVo7aaw0y2shb27GG4trdIiqPQVSRVGzU6ht 8pJPV2MIQqgAxjRvyh8v6P59k802HGKBoWEOmhAEiuHoGkjNfhXjWi02rtttkzlJjTjQ0UY5eMfJ nmVOawL83Py3h836C72cEY1+1o1lOaIXUH4oWf8AlYdK9D4b5biycJ8nC1ul8WO31B35P/l2PKHl 4G+hQa9ekvfSKQ5Ra/BCHG1FAqaftV67Y5cnEfJdFpfCjv8AUWe5U5q2SOOSNo5FDxuCrowBBBFC CD1BxUhTs7O0srWK0s4Ut7WBQkMMahUVR0AAwk2iMQBQ5KxAIoRUeBwJdirsVdirsVdiriQASTQD ck4qTTyXzrfRXnmG4eJxJFEFiRgag8R8VP8AZE5scEKi+a9u6gZdVIxNgUPx8UiJCgliAB1J6ZbT qKS261/TYKgOZmHaPcff0xbo6eR8ktl8zXkrcLW3AJ6Vq7fhTGnJjox1U2/xRcfszqD0ovpj7/hw 8JZ+Fjj3KZ0DX5T8cTMf8uRP4tjwFmJwHULG8t6ytf8AR608HQ/qOPAWQnHvHzS7VdI1KGAPJbSK qnduJpvt1G2JiW3Fudl/la/+qX6iQ0jf929e3LcH78jTXq8XFFntMNOmdTGldTGlZP5f883+mItt cr9btF2UE0kQf5Ldx7HKMmnEtxzeg7N7fyYAIT9cPtDMrTzx5buFBNz6Dd0mUqR9Iqv45jHBMdHq cPb+kmPq4fePwPtRMnmry7GKtqER/wBU8v8AiNciMMu5vl2vpY88kfvQFx+YPlyL7Eks/wDxjjI/ 4nwyY003Dye0mljyMpe4frpjfmz8wb280W5tPLymzv514JeTn+6U/aZAnL46dD265bDTb7uBn9qI EVCMvfswP8ootb8oeZ5m1C4SXR9SQreOrMxWVatHKVIBJrVTSv2sszYuIbNPZ3beKE/USIn8dHvl nqNhepztLiOcdTwYEj5jqPpzBlEjm9Zg1WPKLhIS9xRGRb3Yq8y80/nlomhedYPL5hE9pGwj1W/D 7QO21FUA8vT6vv7dRl8cBMbdfm7QjDJw9OpemKysoZSGVhUEbgg5Q7BqSSOONpJGCRoCzuxAAAFS ST0AxQTW5eT+X/zxbWPzGTQo7RE8v3LNb2V4yuszTKtVdiW48HKkKvGu4365kSwVG+rrMXaQnl4R 9L1rMd2jsVdirsVdiqTebtTn07Q5Z7duE7Mscb+BY7nf2By7BDilRdV21q5YNOZR2lsA8tu9S1C5 BN1dSyr1PqOzAfQTTNiIAcg+dZdVlyfXKUveSx298xRq/oWKG4nJoCAStfYDdsLPHpidzsoLous6 iQ9/N6MZ3EfU/wDACgH07402+Ljx8tymVr5b0uChMZmf+aQ1/AUGFolrJHlsmUccca8Y1CL/ACqA B9ww248skpcyuwMHYq7FVC9g+sWksPdl+H5jcfjizhKiCwPUY6oHP2lND8sFW7fHIsi8ua0t1Ctr O1LmMUUn9tR3+Y74hwtTg4TY5J3TDTiupjSupjSupjSupjSupjSupjSupjSro3kjcPGxRx0ZTQj6 RjwsoyMTYNFM4PNPmKAAJfykDpzIk/4nyys4IHo5+PtfVQ5ZJfHf77RJ88eZzGyG7HxAjl6cYIr4 UUZH8tDucke0Gs/nfZH9TAj5E8svI8kts8sjks7vNKSWJqSTy3Jy3hDhHtDMev2Bl9tr2tW1lBZQ XssdtbxrDCisaqiCijl9o0A7nIeDHubD2tqiK8SVfJC3F5eXJrcTyTHxkdn/AFk5MQA5OHkz5Mn1 yMvebTPynpc9/rdt6YPp27rNNJ2UIeQ38SRQZXnkIxLsOxtJLNqI1yiQSfd+t63mrfTHYq7FXYq7 FWJ/mPOi6RBByHqSThgncqqtU/eRmXo43InyeZ9qMgGCMb3MvsAP7HkVzBe6rIY1Y2+nKaF/2pad eI/l/DNgQ8hDhxiz9SY2WnWdknG3jCk7M53Y/M4HHyZpT9yJrjTS6uNK6uNK6uNK6uNK6uNK6uNK w3zJbiG+PH+7k+MfM9Rkoh2WmlcUk+OGQPGSpU1VhsQcjIOXz5sq0fzRDKqw3xEc3QTdEb5/yn8M QXAzaUjePJkA4sAVIIO4I6ZOnEpumNIdTGldTGldQDc40lBXGs6VASJLlKjqFPM/ctcGzZHDM8gg pPNmkr9kSP8A6qj/AI2IwWG0aSan/jDTK/3U1Pkv/NWNhP5OXeFyebtKbqsq/NR/BjjYQdJPyRUX mHRpNhcBT4OGX8SKY2GB08x0R0M9tOKwypIPFGDfqw01GJHMIy106+u2421vJMf8hS36sEpAcy24 dNkyGoRMvcGRaV+Xup3DB75haQ912eQ/QNh9J+jMXJqojlu77R+zWaZvIeCPzP4/FM80vSbHTLYW 9nHwTqzHdmPixzBnMyNl7LSaPHp4cGMUPv8Aei8g5TsVdirsVYT5n846pZ6nNY2XCNIeIMhXkxYq Ceu21fDNhp9LGURIvH9r9u5sWY48dAR69eTDb29urqcz3crTTt3Y1oP4fLM0RERQeWz555JcWQ8U vP8AH2IauBxybdXBSHVxpXVxpXVxpXVxpXVxpXVxpXE0FT0xpUh1GAXiODsxPJD4HtmRw7U5eOXC xmSNkdkcUZTQg5U5wNqDxkbjceHfIGLMFEWeqX9p/vPOyL/J1X/gTUYAWM8UZcwmkPnHUFFJIo5P ehU/rpkuJoOjj0Vz50lptaLX/XP9MeJj+THeh5fN+pyfDEkcdehALN+Jp+GPEWY0keqOsvJ35geY AHSyuGhbcST0gioe68+AP+xGUTzRHMuz0/ZmSX0Q+PL72T6d+QmsycTqGpQWwO5WFWmYe3xekMol qx0DtMfYUz9UgPt/Un9p+QvlxB/peoXc7f8AFfpxD7ish/HKzqz0Dlw7Cxj6pSPyH60zg/JbyNH9 uGebp9uZh0/1OHXInUzbo9i4B3n4o1Pyo/L9Omkqf9aadv1yHI/mJ97b/JWn/m/af1ouL8vPJERq ujWx/wBdOf8AxKuDxp97Mdm4B/AEwtvLPlu1ZXttKs4XT7LxwRKw+kLXInJLvLdHSYRyhH5BMgAB QCgHQDIOQBTsVdirsVdirsVdiryLzJODrd8/UmdwvyVitfwze4toD3PmHacr1OQ/0j96U1xdc1XG lbrjSurjSrLm4t7WA3F3MltAP92SsFB9hXqflkhFvxaac+QY1f8A5j6DbkraRS3zD9r+5jP0sC3/ AAuNBzodnD+IpPN+aOoE/wCj2Fsg8JDJIfwZMdnJGhxjopr+aGs1+KzsyvgFlB+/1DjafyWPuTC0 /NCzYgXunPGO8kEgb/hHA/4ljs0y7PgeWzIrPXdK1aBhptyJZqfFbsOEoHf4D9r/AGNcMY7uHk0U ob8wpdNjlrSgdS04XC+pHtMv/DDwOQlG23Hk4fckTKyMVYUYbEHK3LBWlY6F3IVEBZ3PQKOpwM4g k0GPXWuSGdjbKEgGyBt2IHc798i5gwhm/kL8tvOPm8Jdsw0zRj/x/wAiVMgB39FD9v57L712zHy5 xH3udpeyzl35Re++Vvy+8s+W41Nlb+reAfHfXFJJifEGgCfJABmDPNKXN6HT6HFi+kb9/VkmVOW7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYqkXm/XhpenERtS6nBWOnVR3b+mZWlw8crPIOp7X1/wCXxbfV Lk8oeRncsxqT1zbF84lIyNlbXBTF1caVsVYgAVJ6DGkgXyY35n87WmkM9nZhbrUhs5O8UJ8D/Mw8 O2S5O102hHOTzjUdV1DUrk3F9O08x/aY7AeCjoo9hkS7IRAb0vStU1W7Wz021lvLp/swwoXanjQd B75GUgBZbIY5SNRFl6ZoP/OOnnC9VZNVubfSoz1jJ+sTD/YxkJ/yUzFnrIjlu7HF2TkP1ER+1mVl /wA40eWUX/TdWvZ27mERQj7mWbKTrZdAHLj2RDqSiJP+cbPJRQiPUNSV+xaSBh9whX9eD87LuDI9 kYu+X2fqY/qn/ONWoQUm0PXFeZDWOO5jaIgjoRLGX3/2GWR1o6hx8nY5/hl80luU80+X5Es/Odk9 urHhb60gEkLHsJXSq7+P2vEZnYs8Z8i6HW9lzjuRR+xFshU0NDUAgg1BB3BBHUHL3RSiQaKFudMS 9IVRSc7Iw/UciY2zxzINBgHmPUB6z6fbyCSGJqSyoarI6n9k91Hbx65RJ3eHHQ83rH5T/kasiw67 5sg+A0ks9JcdR1D3A/5l/wDBeGYOfUdIu/0XZ38WT5fre8IiIioihUUAKoFAANgABmC7tvFXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXMwVSzGigVJPQAYoJp5B5q1ltT1WWQH90h4xDwUdP65vcOLgiA+ b9r6w58xPQckmrljq3VxV1cVpi3nfza2mI2l6e9NQkX/AEqdesKsPsKf5yOp7fPonZ3Gj0vCOI83 mlcDsael/lh+TOpeauGp6mz2Og1+FwKTXFDuIqigXxc/QD2xc+pENhzdhpNAcnqltH730d5f8s6D 5eshZaPZR2kApy4D43I7yOas592OayczI2XoMWGOMVEUmeQbHYq7FXYqp3NrbXVvJbXMST28qlZY ZFDIynqGU1BGEGkEAii8t80flyujo1xpHI6QSSbVjyNoxP7BO5hY9R+wd/s147PS6q/TLm8v2x2Q OHxMfR5Z5z8w/oy3bS7VqahOtLmQHeGJh9gf5bjr4D55nyLpNHpq9RZf+Rn5VJOIfNmuQ1jBD6Ta SDZiOlw4Pb+T/gvDNZqs/wDCPi9T2dor/eS+H63vWa93bsVdirsVdirsVdirsVdirsVdirsVdirs VdiqRec9T+oaHLxNJJ/3S/Ij4vw2+nMrR4+KfudV2zqfCwHvls8iLEkk7k7nNzT5yd2q40h1caVB a7rKaNpE2oGhm/urRD3mYbH5KPiOLm6LBxys8g8bmmlmleaVi8sjFndtyWJqScg7yno35Mfln/iz VW1DUUP6B09x6w6evLswhB/lpu/tt3rmLqc/AKHMuw0Ok8SVn6Q+pIooookiiRY4o1CxxqAqqqig AA2AAzUvRgUuxV2KsU1v8wLTTNRlsltWnaGgkcOFHIitBsemZOPTGQu3n9b2/DBlOMRMq86TzQ9Y h1fTo72JGjVyVZG6hlNDv3ynJDhNO10OsGoxDIBVpd5l84W2hzxQNA08sq8yAwUBa0G9D3GWYsBm LcLtLteOlkImPESLV/LPmaDXYZpI4WgaBgrKx5AhhUEGg8MGXFwNvZvaUdVEkDh4U4kjjkjaORQ8 bgq6MKgg7EEHqDlLsyLfOsP5Nz3f5vXmmXCyPoMDDUZZ3JJe3mYlIuZ3LM4ZCetFY5szqf3d/wAT oY9n/vzH+Ab/AAfRMccccaxxqEjQBURQAAAKAADoBmsd8AuxV2KuxV2KuxV2KuxV2KuxV2KpFqvn by5pvJZbtZpl/wB0wfvGr4Gnwg/M5kY9LOXR1mp7X0+HnKz3Df8AYxTUPzRvZiV0+2S3TtJL8bn6 BRR+OZcNCB9Red1XtNkO2KIj5nc/q+9H+T/OeqahqqWN6VlWYMUcKFKlVLdtqUGVajTxjGw3djdt 5s2cY8lESvpVULZzmC9e7FXnP5m3/K8gs1O0S8mHu25/Djm30EKgT3vG+0ue5iHd+P1MIrmdTy7q 40rq40rzv8ytVM+sR6ahrDp6BWA6GaQBnP0bL9GQL0GkxcMAxfTrG61C/trC0T1Lq7lSCBPF5GCq PvORkaFlyoxMiAOr7R8o+W7Py15dsdGtQClrGBJIBT1JTvJIf9ZiTmiyTMpEl6vDiGOAiOicZBtd irsVeFavfG71W7uSf76Z3HyLGn4ZuoQoAPl+ryeJllLvkXsHlS1Nt5c0+I7H0Q5HvJ8f/G2arObm X0HsvFwaaA/o/fu83/MC9E/me5VTVYFSIfMLU/iTmw00KgHju3cnHqpf0aDMPyztfT0B5yPiuJmI P+SgCj8a5ias+qnofZzFw4DL+dL7mW5iu/dQVrTc98VdirsVdirsVdirsVdirsVSvV/M+gaQD+kL 2OF6V9GvKQ/7Bat+GXYtPOf0hxdRrcWH65Aff8mEav8AnHEKx6RZFj2nuTQfRGhqf+CzYY+yz/Gf k6HU+0Y5Yo/E/q/awzVfN3mDVqi9vHaJv90J8EdP9RaA/Tmfj00Icg8/qe0M+b65Gu7kEuR8mQ4B CIjkyshgQzz8rrIzalc3rD4baMIp/wAuQ/8ANKnNdrpVEDvej9mdPeWWT+aK+J/Y9LzWPbuxV435 0uzceYbtq1CuUH+xPEfgM6DTRrGPc+c9r5OPUSPmkdcvp1jq40qpAUEqs5oiVdz/AJKjkf1YtmKN yAeH395JeX1xdyf3lxI8rfN2Lfxyp6QCg9J/5x40FdS8+i+kWsWkwPcCvT1X/dIP+HZvozE1s6hX e7Hs3HxZL7n1HmoegdiqRea/Nlt5eht2lgaeS5ZgiKQoogHIkmv8wzIwac5L35Ot7R7SjpQLHEZf oYvc/mxBLbyxpp7o7oyq/qg0JFAace2ZUdBR5unye0YlEgQokd/7HnokHIctxXfM/heVp6JH+bNn HGkaaY4RAFUeqNgBQfs5rzoCer1kfaOIFDHy8/2MD1LUGvtQubxhQ3ErycfDka0+jM6EKADzGoyH JklM/wARJZhoX5k2ul6TbWH1B5PQWhcSAVJJYmnE9zmJl0ZlIm3f6PtyOHFHHwXw+f7HomkanDqe m29/CpWOdeQVuo3oR94zXZIGEiC9TptQM2MTHIovIN7sVdirsVdirsVQmo6tpmmQevqF1Faxdmlc LX2Fep9hk8eKUzURbXlzQxi5EAMF1r86tBtuUelQSahIOkrfuYvvYFz/AMCM2eLsmZ+o8P2um1Hb 2OO0AZH5D9bA9Y/M3zZqvJDdfU4G/wB02tYx9L1Ln/gs2WLs/FDpZ83RajtbPk68I8tv2sc9RmYs xJYmpJ3JJzLp1ZVUbIEMKZr5b/LrUr+IX2qP+jNMUc2kloJGQb1Ct9kf5Tfjmuz66MTUfVJ3Gj7F nkHHk9EPPn+PeoeYtV8txwnStAtF+rqw9bUZRymlK/ylt1WvhSvhksOPITxTO/c0a7PgA8PDHbrI 8z+oJCj5eQ6gh7N+W+n/AFXyzFKwpJeO0x8eP2V/Ba/Tmi1s7yV3Pedgafw9MD1mb/QynMR3bsVe EatMZdRuJD1Zq508RQp8u1MuLIShK5JpdXFUPqkpi0XVJRsyWc/E+BKFQfxyMuTk6MXkDxSuVPQP oL/nFy0QWGv3lPjklt4Qe4Eau34+pmt153Adx2XHaRe55r3bOxViX5g+V9S12CyNgUMls0nJHPGo kC7g+3DMzR544yeLq6Xtns/JqBHg5xv7a/U861vydrmi2Yu75Y1hLiMFXDEswJGw+WbLFqITNB5j VdmZcEeKdVdc0u0fS73V75LKyUNO4ZgGPEUUVNTluSYgLPJxtPpp5p8EOacah5A8x6fZTXtykSwQ LzkIkBNB4DKIarHI0Obm5ux8+OBnICh5sftYZrq5itoRymmdY418WY0GZMqAsuuhjMpCI5llH/Ks vNn++of+RozE/O43a/yDqe4fN6j5b0ybS9Ds7CZg8sCUdl6VJLGnyrmqzzE5kh67Q6c4cMYHmEyy py3Yq7FVk9xBbwvPcSLDDGKvLIwVVHiWNAMMYkmhzRKQAs8mEa9+cnk7TOUdrK2p3AqONsP3dfeV qLT3Xlmywdk5p8xwjz/U6zP2vhhy9R8v1vO9c/OrzVqHKOxEel27bD0h6ktD4yOPxVRm2w9kYofV 6i6XUds5p7R9I+35sKub67vJ2uLueS4nf7UsrF2PzZiTmxjARFAUHUTnKRuRstK2AhrIVVbIkMSG QeWfKOu+YZuNhB+4U0lupPhiT5t3PsKnMXUamGIeo79zlaXQZc59I27+j1fTPKHlPyXY/pTVJVnu Y6f6VMOj/wAsMe+/3nNHk1WXUS4Y7Du/W9Nh0Gn0cfEmbkOp/QGA+cPP+oeYZTBHW20tTWO2B3en RpCOp9ug/HNnptFHEL5yee7R7UnqDQ2h3frY2rZlEOoIRVnDLc3MNtEKyzOsaD/Kc0H4nK5mhZTD GZyERzJp9FWdrFaWkFrEKRQRrGnyQAD9WcvKVkk9X07FjEIiI5AUrZFsdirwfWozDqlzGduLkfdn UYzcQXzHUw4ckh5oKuTpx3VxpULrSep5f1ZP+XOVtv8AIHP/AI1yMxs5ejNZA8XrlLvn0b/zi9cB vLmtW+1Y7xJD4/vIgP8AjTNXrx6h7nddln0keb2rMB2bsVU5Lm2jbjJKiN/KzAH8ckIk9GByRHMh 5t+cGpwumm2kTh95JXKkEbUVen05tOzcZ3Jea9oMwlwRBvmUB+UccH6Yu7uZlUQwcEZiAOUjDx9l OW9o3wADvcfsCIGWUj0H3sq/M7VYI/KksUUyM9zLHEQrAmleZ6f6mYegxE5LI5O27azj8uQD9RA/ T+h535AhSfzZYeowSOFjMzMQB+7Ukdf8qmbPV7Yy852XAHURvkN/k9yS6tpG4pMjseiqwJ/DOfMS Oj3QyROwIVMizdirsVdirzP87PL3mbV7CwfSY5Lq0tmka7tIt2LHjwk4Dd6bjbcV+ebnsfPixyPH sTyLpu2MGTJEcG4HMPCGSSORo5FKOpoyMCCCOxBzqOby5FLlbAQwVFbIkIR2m6ff6jdJaWED3NzJ 9mKMFj8/Ye5yrJOMBcjQTjxSmeGIsvXfKH5MwwcLvzG4ml2K2ER/dqf+LHH2vku3uc0Gq7WJ2x/N 6LR9hgerLv5frZj5k806F5S0xA6qr8eNlp8IClqeCjZUHc/xzA0+mnnl95dpqtZj00PuiHhvmPzV qvmG+N1fSfCtRBbrtHGp7KP1nqc6PBpo4o1F4vWaueeXFL4DoEuVstIcIhVVsgQxIZh+WGm/XvNU DstYrNWuH8Kr8Kf8MwOYHaE+HGfPZ23YmDj1APSO/wCr7Xt+c8927FXYq8T85R8PMF2R9lpZPwcj Om0u+Me4PnfakKzy95+9JK5fTr3VxpV8US3HO1c0S5jeBvlIpX+ORkNm3BKpgvC5EeORo3HF0JVl PYg0Iyh6N7T/AM4v6ysHmHV9JZqfXbZJ4we7WzkUH+xmJ+jMDXx9ILs+zJ1IjvfR+ap3LsVfPfne WQ+bdV5kki4cCp7A0A39s6jSR/dR9zwPaFnPO/5xSTnmRTh07njS07njS07njS0nHk+Vx5q0ngSC buEGngXAP4ZRqY/u5e4uVobGeFfzh976Hzln0B2KuxV2KuxVJtf8m+WtfQjVLGOaSlFuAOEo+Ui0 b6K0zJwavJi+k19zjZ9Jjy/UL+95h5h/IW6j5TaBeidP+WW6or/7GRRxP0qM3WDtsHbIK8w6XUdh kb4zfkf1oDy3+SWv3Nxz15102zjPxhXSSVwP5eJZFHuT9GWajtjGB+79R+xowdjZCbyemL2Ly75c 0HRLIQ6RbpHE325gebyEd2kNS2c/n1E8puZeh02nxY4/uwK7+/4pD5+/Mex8tRG0t+NzrEi1jgr8 MQPR5afgvU+2ZOi0Esxs7Q/HJxO0O0o4BQ3n93veE6jq1/ql9LfX8zT3Mpq7sfuAHYDsBnTQxRhH hiKDx2bLLJIykbJUVbCQ0kKytkSGJCqrZEhiQ9g/JrTPT0q81Jh8VzKIo6/yRCpI+bP+GaHtXJch Huet9ncFY5T/AJxr5PRM1T0TsVdirxDzQ3qatqo/at76Uf7GQk/rGdTgFY4ecQ+fdpi80/6xSSuX U611caS2rlWDDqpqPmMaUPMfzB0z6j5muJEFLe+pdwn/AIy7uPofkMxSKeh08+KAKG8keZpvLPmr TdbjqRaTAzIOrQsOEq/SjGmV5cfHEhy8OTgmJPtu0u7a8tYbu1kWa2uEWWCVDVXRxyVgfAg5z5FG i9KCCLCrgShZ9J0q4lMtxZwTStSskkaMxpsNyCcnHLICgS1SwY5GzEE+586eZLuG41/UJYEWOFri QRIgAUIGIUADboM6zBAiAB508JqSJZJEcrL2ryP5d0tPKemG5sYJJ5IRK7yRIzH1CXFSRXo2c9rM 8vFlRNW9Z2fpIeBHiiCSL5d7zH80pLRPNs1taRxxRW0UcZSJVReRXmahab/Fm57OBOIE9Xn+1hHx yIgACuTMvyi0Kwm8uz3l3bRTtPcMIzLGr0VFA2LA965r+1M0hkABqg7XsXTQOIykAbPUM8g0jSYJ VlgsoIpV+zIkSKwrtsQK5rDlmRRJd1HT44mxEA+4IvK212KuxV2KuxV2KuxV55+b+pS2sGmQK5WO dpmdQdiY+HGv/B5s+zcfEZHup5f2mJ4YRHI39lK35SasLrTL20L1a3lV1UncLItNvaqHI9o4uGQP e2ezU/3Uodxv5/2PENfXVY9cvV1dWXUvWc3Iav2ya1Ff2T+z7Z0+DgMBwfTWzpNQJjIeP6r3Qatk yHHIVVbIkMSFVWyJDEhVVsiQxIfSflPSv0V5c0+xK8ZIoVMw/wCLH+OT/hmOcjqcnHkMvN9C0ODw sMY9w+3qm2UOW7FXYq8G1JwfNutW7Gi3N1coP9cSsVP3jOxxx/cQPdGP3Pn2s3z5B/Sl96WGoJB2 I2IxdfTVcaWnVxpaSfznop1jQGaFeV9pvKaEDq8J/vUHuKchlOWPV2GhzcJ4T1eTVyl3FPoP/nHf 8zojCnkzVpuMiEnRpnP2gfia3JPcHdPu8Bms1un/AIx8Xa6DUfwH4Pe81rtEPqMs8Wn3UsCl544Z GiRdyXCkqAPc5PGAZAHlbXlJECRzovmW203U7u8jgS2laWdwg+BqksaeGdnKcYi75PCRxSkaA3L6 etbdLa2ht0+xCixr8lFB+rOLlKyT3vewjwxAHR85eb01K480apNJayqz3MlFKMSFDEL2/lpnXaXh GKIvo8RqxKWWRI/iL3PyHp0mneUdMtpY/SmEXqSoRQhpCXNff4s5nW5BPLIjk9Z2fiMMEQef60+z Fc12KuJABJNANyTirFNF/NLyRrPmGfQNP1FZdQhJCChEcxX7Yhc/C/H269RUb5dPTzjHiI2aIamE pcIO7K8pb3Yq7FXYq8i/5yAuDD+gaftfW/w9HN/2HC+P4fpec9oI3wf536HmvlzzlqWgapHqFk3x L8MsTfYkQ9Ub55uM+jjljwl0ulyywT4o/wBr2iJPJn5o6KZ+Bh1C3AR2FBcW7NUgE0o6Eg07H2Oc 6fG0U65xPyP7Xp+HDrYXykPmP2PJvN3kPXfK8/8ApSevYsaQ30YPpt4Bv5G9j9Fc3ml1sMw22l3P O6zQZMB33j3sfVsyiHApVVsiQxLIPI+l/pXzTp1mRyjaUSTDt6cX7xgfmFpmJrMnBikfJytBg8TN GPn9276TzkXv3Yq7FXYq+ddekf8AxDqMtf3n1uZuXv6pNc7nTgeFEf0R9z53qj++mf6R+9UvCsnC 7QUS4HIjwcbOPv3ygCjXc48x1Q1clTB1cFLS6KZ4pFkQ0ZTUHEi1Gzz/AM+eU/qcrazpsf8AuNuG rPEv/HvK3VT/AJDH7J+jwzElGi7zS6gTFHmw+KaWGVJYnaOWNg8ciEqyspqCCNwQcgQ5gfTH5Q/n rZ6zDBofmedbfWlpHb3z0WK67AMeiS/g3bfbNTqdGY+qPJ2+l1gl6Zc3suYDsHYq7FXYq7FXYqsm mhgheaaRYoY1LySOQqqqipLE7ADEBSaeTecPzXivrSaLQ2P6OYtFHekUNy42cxA7+indv2zsPhry 3fZ/Z5MuKTzvavaVjw4fEvH9a0FtTk/SukE22vQsJXhiPD1mXf1YSKcZgdyB16jfNnn09bjk6rT6 mti9Y/J/874tbMPl3zPILfXFpFa3bfCtyRtxfskv4N232Og1Wk4fVHk9PpdZxemXP73smYDsHYq7 FXkf/OQ0TGw0WXj8KyzqW8CyoQPp45v+wT6pjyDoe3R6YnzLz7yN+XOtea5w8YNrpaNSa/cfD7rG NubfgO5zba3tCGAb7y7nV6PQTznuj3vobyz5V0Xy3p4stLh4KaGaZqGWVh+1I21f1DtnJanUzzS4 pH9j1en00MMeGITO5tre5ge3uI1mglBWSJwGVgexB2OURkQbHNtlESFHcPIfO/5MyRepf+WgZI92 k01jVl/4wsftf6p38Cemb/R9rX6cnz/W85ruxq9WL/S/qeWOkkUjRSqY5EJV0YEMCNiCD0ObvY7h 56USDRep/kXpJkvtQ1Z1+GBFtoSehaQ8np7qFH35o+2ctAQ793fdgYLlKfds9izQPUOxV2KuxV85 69/x3NR/5ipv+TjZ3Wn/ALuP9Ufc+dar+9l/WP3u02VXV7KQ0SY1iY9FkHT/AILpkc0f4h0ahvsp urI5RxRlNGB7EZAMKW1w0h1caVfHLx5KyrJFIpSWFxVHQ7FWB7ZGUAQyjIxNhgfmn8v5YBJqGhK1 xY7tLafamg8durp7jfx8cxJwMXc6fViex5sJrkHNen+Qfz+81+WljstR/wBzOkpRVimYieNf+K5q MSB/K4PtTMPNooz3GxczDrJQ2O4e6eWfzy/LnXURf0kum3TU5W1/+4IJ8JCTEfofNdk0mSPS/c7H Hq8cute9nVvdW1zEJraVJ4m+zJGwdT8itRmORTkA2qYEoHVNe0PSY/U1TULaxSleVxKkVflzIrko wMuQtjKYjzNPNvNP/OR/kbSkkj0n1davFqFEIMUHIfzSyDp7orZl49DOXPZxMmuhHlu801bzR5u8 78b3zRM1h5dJD2mhW1YhcUNV5ftsm1S7/wCwAzbaXRAcvm6PW9pSOyFubl7iQMwCqqhI40FERFFF RR2AGbmEBEUHSE2pqxUhlNGG4I6g5JUJrvl+LXq3Npxg15aHrwS6p4nosvgf2u++YOfBW45OXp9T w7Hk9C/KD8839WLyv5ylMV1GRBaanNUEsDx9K5J6N2Dn/Zb7nQ6rR/xR+T02l1l+mXze85rXZuxV B6ro2lavbC11O1ju7cMJFjlXkAy9GHgd8sxZp4zcTRa8uKOQVIWERb21vbQJb28SwwRALHFGoVVU dAFGwGQlIk2dyzjEAUOSpgS7FXYqxrzP+XvlrzG/rXsLRXewN3bkJKQOzVDK3+yXMzTa7Jh2idu4 uDquzsWfeQ37xzTXQdB03QtNj0/To/Tt0JY1PJmY9WY9ycozZpZJcUubkafTwxR4Y8kwypudirsV dir5z17/AI7mo/8AMVN/ycbO60/93H+qPufOtV/ey/rH70BlrQmfL6/B6g/3shH71e7oP2h7jvmL KPAf6JZc/eg65OmLq40rq40q6OV43DxsVdejDrgItUq17y95c1ZXnvITZ3p/4/LUAF2/4siNFb5i hyg6e+Tm4dXKO3MMMvPy+1dSW02WHUouwjYRzU94pCrV/wBXllMsco8w7CGrgeeyQXulapYsVvbS a2I/37Gyf8SAyDkCQPIqVveXVs/O2mkgf+aNih291IwEAswSEyt9R82X1Yra5v7rl8JSN5pK17UB ODgj3KcpHMo+18heY7hvVvwmno27SXj8ZD40iHKUn/Y5ZGBPIONPVQHW2RaZ5f0HSSJIkOo3q9Lm 4UCJCO8cPxVPu5PyzJhpf5zhZdXKWw2CNmnmnkaWZzJI3VmNTmYABsHFWYUOxV2KqWs6BD5lQceM WuKKQz0otwANo5SP29vhf6DmDnwVuHL0+oMdjyfQP5Nx+ZY/y902LzEsi30YdYhNX1fq4ciLnX/J 6f5NM5bVcPiHhey0vF4YtmuY7kOxV2KuxV2KuxV2KuxV2KuxV2KuxV87+Z1C+ZdWUCgF5cAD29Vs 7jSf3UP6o+5881o/fT/rS+9LMyHGXxSyQyLJGxV1NVYZEgEUVBR7xx3sZnt1CzrvPbjv/loPDxGY /wBBo8mfNA1yykU6uNLTq4qll3ceq9B9henv75MCmyIpQySURDqOoQDjDcyxr/KrsB91cgYRPMJX jVtQAp6v3qpP6sj4MO5PEXSatqci8XupSnTjzIFPkMIxxHRCEJJNTucmh2FXYq7FXYqvhhlmlWKJ C8jmiqNyTgJAFlL2f8uPy3XTVXVNUQNdsKxQncKD41zm+0e0eP0Q5PRdmdl1WTIPcP0l6Pmleidi rsVdirsVdirsVdirsVdirsVdirsVfOvmJy/mDU3Oxa7nYj5yMc7nTCsUf6o+5871ZvNM/wBI/el2 XuO7FV0cjxuHjYq6mqsNiMBF7FUd6ltffbK295/P0jkPv/KfwygxMOW8WYIKFngnt5PTmQo3v39w e+SiQeS0gby44r6anc/a+WTAZRCByTJ2KuxV2KuxV2KuxV2KuxVMtE8varrNysFjA0hJoXp8I+nK c2eGMXItmPFKZ4Yiy9s8lflvp2hItzcgXGoGhLHcKfb/AD+/rnM63tGWXYbRen0HZIx1Ke8vsDM8 1junYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq+c9e/wCO5qP/ADFTf8nGzutP/dx/qj7nzrVf3sv6 x+9AZc0OxV2Kp55W8o6p5iuuFsvpWcbUuLxh8Cd6L/M3sPppmFrNbDAN95dA5+h7PnqJbbR6l6zN +Xvl19Ki08RMBCtEnY8nJO5Lf2U9s5uPaWUTMr5vUT7GwmAiNiOv63m/mL8oNbtHebTm+uQ7nj+3 9w/z983On7Wxy2l6S6PUdk5sfIcQ8v1MGvNM1CycpdW7xEGhLDavz6ZtIZIy5F1hBHNC5NDsVdir sVdirsVTTS/LOuanIqWlpI/LoxBAp4jufoynJqIQHqLZDHKZqIsvRPLf5MNVZ9al26+gn8f8/ozT 6jtgcoB3Gm7EnLfIeEfa9N0zSNO0yAQWUCwoBQ0G5p4nNHlzSyG5G3odPpceIVAUjMqch2KuxV2K uxV2KuxV2KuxV2KuxV2KuxV2KuxV836u5fVr126tPKT8y5zu8IqEfcHzjUG8kvefvQmWtTuuw64F Z55Q/K+81ApeayGtbE/Elt9maQf5X++1/H5dc02t7WjD0495d/T9rv8AQdiyn6su0e7qf1PWbOyt LK1jtbSJYLeIcY4kFFAzm5zMjcjZeqx44wAjEUArZFm7FUNd6bYXi0urdJqilWUE0+fXLIZZR5Gm nLp8eT6ogsa1D8rPKN4SwtjA3/FZoPp7n78zcfamaPW3XZOxcMuVxSG5/I/S2YmC+kQHotBt9J5Z lx7bl1i4cuwe6f2IF/yNk5HhqA49q9fwXLR22P5rUewsn86P2rovyN3/AHuobf5PWv0rgPbY6RUd hZOsh9qY2n5J6JGwNxdSTDutKfiCP1ZTPtqZ5Bvh2CP4p/YyLTfy68qaeVaO0EjruHfc/eKfjmHk 7RzT6ubi7HwR5gy97IYLa3t04QRJEn8qKFH4ZhykZczbsYY4wFRAAVMizdirsVdirsVdirsVdirs VdirsVdirsVdirsVdirsVfNV8hS9uEPVZHB+YYjO9xm4j3Pm2QVIjzROjaHqes3i2mnwmWQ7u3RE X+Z26AZXn1EMUeKRps0+mnmlwwFvYfKX5e6VoYS5nAvNTAqZ2HwRn/ipT0/1jv8ALpnMaztKebYe mHd+t7DQdlY8G59U+/u9zK81rtXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXl035VahfeZr2aeRbbSXnaVHUhpHRzz4ovaleNW+450I7XjDDEAXOnl5 dhznnkSax3fn+Pe9E0jRtN0izW00+FYYV3NN2Zv5mY7sfnmjzZ55JcUjZeiwaeGKPDAUEblTc7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q== + + + + 1 + True + False + + 512.000000 + 512.000000 + Pixels + + + + Cyan + Magenta + Yellow + Black + + + + + + Группа образцов по умолчанию + 0 + + + + Document + AIRobin + application/pdf + + + Telegrator_Logo + + + proof:pdf + xmp.did:a212ad59-f7f9-4445-bb5d-7f16dfae02a2 + uuid:59f69d04-496d-49ea-965a-4b205ad9010f + uuid:ef810197-bab5-40a1-8a53-7e27e8c3d974 + + uuid:ca6d3c91-6cf3-455e-8895-3f52108d1bf7 + xmp.did:af2427c1-39bd-4642-9d60-e8e045f8eb5f + uuid:ef810197-bab5-40a1-8a53-7e27e8c3d974 + default + + + + + saved + xmp.iid:af2427c1-39bd-4642-9d60-e8e045f8eb5f + 2025-07-24T22:09:56+04:00 + Adobe Illustrator 27.0 (Windows) + / + + + saved + xmp.iid:a212ad59-f7f9-4445-bb5d-7f16dfae02a2 + 2025-07-24T22:10:54+04:00 + Adobe Illustrator 27.0 (Windows) + / + + + + Adobe PDF library 16.07 + + + + + + + + + + + + + + + + + + + + + + + + + +endstream endobj 3 0 obj <> endobj 5 0 obj <>/Properties<>/Shading<>/XObject<>>>/Thumb 40 0 R/TrimBox[0.0 0.0 512.0 512.0]/Type/Page/PieceInfo<>>> endobj 24 0 obj <>stream +Hdˎ$ EVcA+ga z-`F eLW`~?<}wKhq5/)s +/o&8Ӓ[FK-9.XQ%Ly!-/4p㥰$6MVf.VV!.囦, +!6y dBi?4>uBFEfcDYo7 |驑\&,&oƧ \B]Y菉&. @~v7v==5 +t_b)Ғ +/do +Dt Z&X/3Ee9y||L|四{T?K_-| x#BޟO_}ߦ8ҟnIni:!~vƭ:M?}N/;_WT)8{c3DMLhh!wSz(AO1rmĕKOa =d.{)MJ )Ӌ5\p ͭWG^uOWPtELgy8&1ó׬M_C6ŋ1lMA= +RrfeUku|Y'=ztH4a+WTP|fu]ƂP¡vKm4.+G+v$+6uQc`#rӈp TiW$G7(~6Hnybtnq:ծyE/py&x DӲK#kiGV]) y;aC3ڴIdALI$*uqcCհYJNFo_Ɩǖ *MɲDC$Ԯ[ ǐ]m>˜jSa1nԬSbpb6J葍ni.G CNdGf!@j :ٶG17,ۅkHiZ:+~L'pAU3qߚƢDENo<|\JllxjbI_$[ $H Ĝn +,P*Hi + Iэf1"@g +AKWoO K! [8DԐRX4Be%A``5-f ҽDAcn,DDI[0 +,w$2rR3jN\|<4*4 +'Qe$~Z"?]0 +alvFcOeF:X (@+&hIe=j[BTmV:ei F1fwLmT15ڢHbJ&aM3)b|C R3Ăy, +[]Rs)Io6n3 W=U3p%vpPQmݩVԬCS¡19 ]c˫paU2lz8FM>Na +%g ow}],"S0*`ڃQ04mC[59ņC6:RЬuG#ҥnvl64WFZ/z29:YMГJUWMufȋ$TQY,j3h-γF@uhȦ'Am>s*Q 9o wլ"n!"Udi7&$m("Hb%oi66p&B~z8J.@7V3IsN0 +hdjCmW+D +4ejAC9ipF|xTKmiDh0j9r+;J-Ӗ=@aځ`jrB4!FB~_ AѺIt0۴ajAIt߶_ị rUj f>*Ppn)Pos&~|f:` 3zN !D$vU7F=}ִP6N؆i +Tu3g@`IٓJ7k\!4Sh% /h9QE,E=ff 7 Dp& y@t́\rl(ڿЋ("ٍeHO܈2YU]EP!ʹ.ֳoŒ|pKMS{,I+JvE.Tk#tGRcv4|p'1&r1e! +p# D̰(S t>g!EMcu_C嘬DNNo[Mޙ~Op.!Zl[F + Eɹ3D6"w*]`mwt;|6sS+~ =x^vrzw3 ɑhY󺶅/(+: +gK`m[Rwr4L%.pH;}hZ井-<%΋ +j/`$9H=B2;X-5YQcFZ_c veF*/Pܷ3/F}=I'bA{\(.ou{)duEw0hA&*#yF,9=m>=}bi`d#o)T5$l;a0Rr೙{Sl3/Z XiϮ@ē+VGh9q,&76{ MQvXdDbP~$=`::;enq%'1p׈!KG;Ώp\Exش[ , 5M;* 3mf[Q9]KpuN>!I8FJJ/**(ԩu٢JqeQol:HkvǹTTS$8H|#+NAN~Ik3 HB3kGD;cv74 ݱzц +^?*tʠ4rڳ9 Rijꧩ zJ/hY(CGbOx{(Ώu[RHKfBe[}Sс$q|U'LX*k@N#jrl+8H:"^?ndCE6rzCGr\wK_|^iCr A2]{wp%W@ VPdY;},SU;MtDp"0d WC4vgq#!_TN/W8B}!-bY q2'ȷ_GEF=(TD ,`-b"2'Pd+BnW\ߢy^42d?})Rjr+&5fmM* +EG!cʽ0WaqLXL:Š4P(~t'E,~޲g]tP:nV雈?=bMhDi}y~="!PdϹ|&*8 nE; %anw+sy ۢ۱-uDqZcQ4,+m߷ps8rX ?8SE_%,y틤~h +j]{y +摲_L#9L+0{v^K9F`TO37qܳPl{MU\*pkxW{8~_qؑyPX?Wb[`bkBZa;oq)xؒ-~<\o9jI<t|l7uU\ϿQ~~_g^|P@>O<6J oz;Bys;q/GGr7_~SH &]ro$?# P\)ݐds"OwTN߄AF =0Ojì-z`j< +GΘڒyPL? E&@?;lۇZ|x47MHb^g9Y l]b$eiEEvئLǞ+Լ[ V\JWT締lZTH9%N'yYV +Nٵ45 95TשD mix{D#KxmiQ+ )+ Q\0atZ}+}KM qV?=MwȬp=+gonxmC_# +j//{wOa%ͻl +Uzy4cOᢻ1mEKIk=[jWk޹.q|o fᙙgKw=L*N4Wp,8Ow9D\Ntv6'ld80S@)aR QMA\S zOPiSnZv6SE-]ivCK7?:|яzcFլWFct};Ca +AI@"ksj4ElPp͏P%5{MoXOyfNGyZsà$DA>|۪\8GhL͹:ϧu=[䲝VS^J$d[6~Xb^cmZ au :*Y]Q>>RX/VteP Fe.NN?sDIl-UzT1 =Ġ~XȃB'劫6c8}_r<^#D^LYMl^7=!lhUdqO+.i1 : Tw%H@<Niy^dkE]49 =`fT83idh d끔O64ڼk!+To4<7<~z6HJI [L#v" +.qp ,ls7ȏ#=Kpw@lv dhМO4G I;.)5BnBkIuaz4 ))bٮdC_ ?Mlobc?\7ɺX,Ie{Fy=:jݐppwށsc\!Fpݬ:aбhGb+WXpF C|8^i',Џ9&`3v'yxƔQs탑I|-% +hӚ{OfFol}FSF`̠CULVRCl!nBT)FaCm?Zs(iq LE"RAu4ڍiNΚ+tfjw1d1ЕL8@؟I;Bo,eI@R4ThxڒW(t UA-/of!%wXIY 9CWU3_,(y<;oLar՘H4%C ⌣8LɊc7bp1|Cy%cywM{Ĝ+ tt4ȚH=957A)8a +I=#<a{8ڼx`n|3仇=Mͻ`7S-ڥx{+rJ +m%y8s̬Gdh )$ .=u)eRi0f07ޝQS'X+9/TpI*)"܀Cmqg-S[8巅!>=xi=-%rL4pvbobi`224hTܩKfg7>Ӗ{Լ :)?ǕDm,NglIo_Ox(R`ٴΣs݌_r0^Q4ǯLΣ:2luݤH^s/BV;ӷ0BG˝d/3W?_1`JplT׆r j4nq^m)h+& cr(3;_b,%l _,䍝^-qϊ"*n;T[D6 ](qr<:anKiL,fwk2㿏.I; {࢒4 txb)t!QiY 8bH,bغqLz-"5Ӣ`ZgUE0Wy5/Yc&^!oA!{M‘֖_;=nTk%ZSv9>5m*=|ey}_z9O^#nk^lG`[6,}lw .& B93S*⭈vftX| T.U:F'^>G4w >%Dmv\l +S3}af ˺%IDͰUHqP: \Ӷ![`<`z9ݔv%7g r +EeY8qK"Ĕ7JAX +\JO +T-{[Bnlzj7b'^֗7mCHXP,捱|cC]/u,w-N+ ]AN=unOYM뉓&j@ϵ"3Yf⚑qCӝ} rw .tɬ!F|"4E&h|*N#쮘pw˧->i.ᱯѥ݂nyzu#f>AW&p-es9Dc7 >N_e$ +bE>2#/=F.5hn$%:҃s :`("mjp5*z]%AzȇT8q1oA\MR!e,l\3%VUn|@;BT6 >qZ^Hn 1vv0#=-q(N!µnJV +ZC[ mįzbjxT]O%tŻ|r ֭͂Mwq$~ۯ?:1}IПYo~LSdzU_~{ϟ{}J[_s O +endstream endobj 25 0 obj <> endobj 40 0 obj <>stream +8;Z\54-lBj&.D:mSdB""n0qrh(_P*tb?b;tE0re[Y2efu#8!N4G=,U5C12cfeQjb/ +kPgOkd?!>?O.W;aNU7/lhqI?OimgRYqF0E`P,S6=UNV$I<7P#S[4QgY5R&HoBa;0R +:3()r+&LS)r<+V`LhJhATnbCBX5utGrC0b?\U^sM^WG7p?'T2Oh"p_5Coc\02+2F1 +._*WR$=p?mdq1moY=YaDd$)A;gh595UH/G>&%s>.nJ[1(&M#W7\?!?YLQ2o.*a"-. +K9fLG7;hE1aL<'Cr\loGkt\WjrON3dK50iGOCb@qr]Y?XMeO(nD8HM$r?9a>@c1- +A]b?l,9'n;;VC8_gc=fbZ3KWijO-uc!hF;*^5ig%`,`XK2Do[\(S9B&^)f1h>IpWg +;L:b+1#Lq_[=S]g#X:t`9mir.L88-@GOYYN%$t%nhRP)b50_eiFSo<*c6.?qi7`W.(8.k3C(gB#>[575c3U!>(k3I^+m_j6UhPjI<2>\]Q&c5B?MSA3L]A +>as/H)e_1:IX3uHH<=X7B@OC;+=2MsiKAZLVtpe'hbMh]a0`oQl"FFCT^69'XDa!g +hs&fE_\Z]_\UR&gB,B\8?0m%QqJSHra9LMJhuL">7t"%H!U)&>+eBe>_`g=*O:A:r +$9uu)X3.8'"3klDB&MGFQ[3#9s*E&kH&W:bpg#\$I;pAf0br<-O2V<_aDLS"!=:ag +bQ"k)aMG,i#2KgAGj"aZ3hS4,#N6X<:T-o[fdCU$5#R%9%EJm(mtr;S.TP--D]j.k +3[MuT4H4D7X3.h$(_T^TLsX+&2d +qLID$I/=t$oM*Op^-$mD\K=[3*""c/IU75qs$pjcofl/Ps1^f8s1cAKf>m=\!%aWF5Q~> +endstream endobj 9 0 obj <> endobj 10 0 obj <> endobj 11 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 24.0 +%%AI8_CreatorVersion: 27.0.1 +%%For: (Rikitav) () +%%Title: (Telegrator_Logo.ai) +%%CreationDate: 7/24/2025 10:38 PM +%%Canvassize: 16383 +%%BoundingBox: 20 32 502 480 +%%HiResBoundingBox: 20.8923579645134 32.9732530212496 501.22825507038 479.026746978749 +%%DocumentProcessColors: Cyan Magenta Yellow Black +%AI5_FileFormat 14.0 +%AI12_BuildNumber: 620 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%RGBProcessColor: 0 0 0 ([Совмещение]) +%AI3_Cropmarks: 0 0 512 512 +%AI3_TemplateBox: 255.5 256.5 255.5 256.5 +%AI3_TileBox: -29.6600036621094 -150.559967041016 541.660003662109 662.559991836548 +%AI3_DocumentPreview: None +%AI5_ArtSize: 14400 14400 +%AI5_RulerUnits: 6 +%AI24_LargeCanvasScale: 1 +%AI9_ColorModel: 1 +%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 +%AI5_TargetResolution: 800 +%AI5_NumLayers: 4 +%AI17_Begin_Content_if_version_gt:24 4 +%AI10_OpenToVie: -185.27659574468 624.510638297871 1.30555555555556 0 8171.6170212766 8186.17021276596 1107 932 18 0 0 50 91 0 0 0 1 1 0 1 1 0 1 +%AI17_Alternate_Content +%AI9_OpenToView: -185.27659574468 624.510638297871 1.30555555555556 1107 932 18 0 0 50 91 0 0 0 1 1 0 1 1 0 1 +%AI17_End_Versioned_Content +%AI5_OpenViewLayers: 7777 +%AI17_Begin_Content_if_version_gt:24 4 +%AI17_Alternate_Content +%AI17_End_Versioned_Content +%%PageOrigin:0 0 +%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 +%AI9_Flatten: 1 +%AI12_CMSettings: 00.MS +%%EndComments + +endstream endobj 12 0 obj <>stream +%AI24_ZStandard_Data(/X|j7HL-6!2NQt.ɲ,{0!H%zZAESS,|>p81f}PI]mGP$Lj&ő$K0ͦpԶ<󹵍 S6ɓ$O4O<<ɓm[C=o{o"3;[9('OYj/ϯRoAnλԣY{nq9![b"9_ԛaowp^[ E0˜EPۇ_}[kz%9aH&u޹gA=fCs);]vچf3Y`ysԶru2}^ ݃sX揊z3|;?ue\]o9"ȹ(9-e΃[{;vCumڶ9h&x(C`a(cHehgy\틠8Xxq|(Hh爎&9? !)#IV&y(K]/2,r,ɲ,,2&7ofhhfiif>O~#Y' YmɬwYxg0yd.ьfAԧF{w~nܧexr箻]`_/C}gvc"&7zԤ5z\19>3Z/}祶#跶e2xXc8#8>=1h) +k98wp0ssw`\em9{< /k[C r/jO6|5z$}Koӣ?_o~G5APe_E0GRԶ"f/:Ӡ}9 C_ߟ Cm0Jr#Z5}cbV[DK%Q~Lr]cª(V]o:eQVe QsMW#TߟJ*+(~GW[ׁ@ ԰C-9=u=_YyOvoޫq`j֞o +ĂY3 fA3 .M`V!8 j8VG+yP Q]),sWUd,v=ʮ({N2V)Kr,4nUW +4˺% + ŭ +\M`O; fwطޙAۜ\E-Nع=97ive2:m!èXa8eЦrL֜y_FSuɚ~\*FH8kDiDM񚱊$uѮ6uWUI3von"n1,cYa^7Nշ:m-eQȋ Vq2,Ocx}т`FW/.d:TDLR#|$ݐ7AJNcv<] AhX:m)R& T3,giF(%64Ǘ͈RP9`V3d)sh޷TQI=hr17BDY 86 C4@Pr q鈄-vmp?Pqהu&yc CJu2g<ӘͦmY4fQ +STӶ;y(DZǐKFst$̆#;XP7SVhEp bR:| zd=_5 /IaF_6"88)˗aaHurDj;Ž4,(ztHAxm ގÅd;:7mʵ|XsƔ[f :dpĖI1 +}KK]@%_T$|ӶgL }M +pAV6%^!z6XCyܗ {'_!!;rj~XmE%[rv8D81̪,Ɗ*j-%/ЋCX4ޕ_{ٸU }?~]RUU])l塇}ʶ>ֻʬObXxU./b25ج*m8^}/@4Rae\ +p,{V2`b 5n/րZ-9ꃩ\cܮ^*7:kqOToL9s X f:0`I7{1\b]>jr9o̶o=<'Qy@9n5pWl%c/֠mwe\,Ʋmd-Veq~UR֍[w^jlc_ʮXU+k}z_qCڕ JW#]_1j$\KKW+o +z<ëKê[6, `C *h6(e`nWi]+J3`f6.]`F[p̸ +3<g7 lp ؐ`6(`V6,0*u`&3BL``fܷ3l `Cl `lIQClL0L +Y`YfM0+Wa flp!j=+ evM[5N_U9MYmڥ"RRgԕR'b8Ectg)Yeɕkw_e׈#H_ֳT徫QONezmq<-#H_]׆/rUۺ ݈ׅ ~Gjeݐ +K<( m[\Sk.!=#WY]}`U0lP`V!Q2 s[G2p,jՕڔU Xe +*u+aYw\%G=ci8]ّ[v3>p^w.\B̍GǙ .~G p8U]=r4^|SP)Wss[ naUjTܒꮆZ]Iʪ#®ȍ`UQҖj\5lH1 +.q ؘ`f, fذ`v 3Ѩ=cs߲!p𨲪4~XVS%c`y[!=|DܮoUUju%}IJjnR?*j=-^/^,uW~W4sjGuqKbk}C,U;=䶨C5npZbi/.l 殸ʮ`X}c0peeg̐4PcNmW6y-Q3lʭqJcjƩ[UX%MC1Cq ^۔2XUE.i\RWVVCЂrћ2P[e+1,[M" 0pqjl  TǩMb*ՑʪM4NYi/\8i[UYnkQYnH8WMXTr>RoںkZ1i[jOT}!Cڶ + +qi՞ \K5/%pqJ洶J\X\@\01pY"(qYƒmiY1:h[SaÅX0` f삙f.I`V!6 B mU6xcT ;?{{k2r/jA{pk j-jc6m mGG?m#YNOhNb(j(~sqg ˰0 C05 +%H#8~Ѓ],/wӟ׶1}e';)~m5Mmkpk~ϒpkz%9ZBk4Km#G1Y۾\v9[{ C-˭mk!8jQ\#O~2$C4Gmjh&j<<=݃mԡhj[G݃͠?AqIPIǽy{cmb(d8exY^)hԶEQ͟ÜѮm{DM?$zQREe 24N@Hu١.'J_i)fTP xbyCXK+G@% *U9,ɌJҗNBb 4< oq34De a0 Z l=IlJ40fׁ3qJ];*%]()憄L2F6H7-\oPR9\vڶ/ [ -D6&58NHqTt)4KǤSy"3YLD Q25&%{'}M%& C&ҁ|d"J!1r"F1Å,u&0NF'&abӁ 8=6g-v9UR&Xɐ^~Qg[[,z8qHtN"-F΀3`5$NM,XhJ*UH$4!0h"C`4 !v1hd &1x?U"w7Jt:KK&ę- )<:S*yqzya1'%s:6+( bV۬073 ++Ca4R4B0EQND5bqD>s(@!*~a"I A# iۈtY+=rh0N \[RPMGo ~i[)2+o96#UqDSd8.hqZ.xW<:~C64fWf. Q hD;m;D ٗ,/_ L%S*_EqJ%OL+K0.t|Cr3L1S3q2t:uY>O#"&[%q\Kd\D}F.atҒYDD#N;8hTmR\jR5[Pa!NC6"g1l &Ҡ3` Z#0x` D[H4ۃq~*bwL v3DnMQx:bw\1_d;tBd#Ò0hbaq0csXHO'<2G@b&N;өct^S<2f3+(2YWc3Rt  eD + u,<<ԃ%* CAIUKFw;hvb &x DhP贍^;8~>f8M2D*IE +% w!Q G!M*$]* Yq6%8>1xU HF 0X 9- +,H.$&]6D $`ɃFH_C}eK!R$:!|" Gc# Y,;mN +3„R&PtHAoD!}jA+]yjADžcȒ4:m(cٮrIJ'196Bpm/!2%c0C+&dơ@I?N(jDZP@:m넚A^"BGF~+NN !z: up(DF om5,Tx`}tY$[tNK N '0s h$̉[Xl9! ]VT@CBqz#X!! ֝A T$ Owި{)bJ^{ sE֜xEO щb Ce4Jʁz,GbEGB"Y9t '0ʎ,6BaB"Dz!9+R iDŰ|Ϣ0!_HNu6ECXA0yq {e;ngҽp :Dkp iahb#Le҂|AXYsJ[+&Q; J +}/Nr-Kήy t3qCGR>pIYcFT*b[%yXѓ.HEE#RCj~IG?zKh'$ U A0ڙl+GOiB#(alKxn +K@hBf/>J +DX +xRig<.t#ZĹdlSܔ +?Rr.( KfT'_ѕ/N4䶔 %B-d&`FNɢF-H"xF;>QT *UD=2.9rUt'|Koe5Ey.|PPikyL:Į4҄yߕy0y-K>56$, 6/qX \2 ?򩡜eRp-N&TD#]' (<6U&ގLAq<0+mvR'{vSdom0Dk#-r*TfNrAb%]3rv:\ߔ |t$}[:mJŠ $LpZ'Cp:mQdb6V8 +Cd -֗/n"ӷȔd?(LuсT4T(aN1DTLrby}¤±q`R :^gNL_ +Oc>{*c53({hm@X[1Qոɱ`"'Ν?d;K&݉|!!iQzpcKסZٵPB{$XX&f0$`lI<ٝ:&BUVjxG³xU %]@Hj&) +Ca!ve촭Jˊ66eKqm ,#uil?q: %K  LB0ćLآ #| +s.K-} +J1#^KND %Qr2&I"m#`HhN(|'~ƃ]jp3bXp̓Аi[6הVN/\;Db9Cpx*<^XPy)ѥ6٘RҎ%T +ÂS$ Ix?UvږЭ|K #qak +$2ޖ}Irs!$lo5I͚("BpgulB4c8@MGI]:# i:"B28qlD ZeUOx8jLQCڰB(';Ccv~@OYߝgs y4Bi8_نCTRtpCHl^J +38́w~pۃ, mFԮNJtk֐)]J(YmL!NsT0ۨԖ;ׯ(}n@Ao(:EBk.J3ՆG1ٵ 2Qh5J|qDƷV"~9uWB +b|.0M1*!aPt9lggZRJ`Jbu$8RøSQr,a@ ifu[hf):WFm뼯D3ɟѭ4uC_VWcc1咹~z_۷+s$y UĩS#fϸHAXk 9_0܃QzˁbA'>KKԿp)c u~ʳvKa$i!Bbw>`\Q<<( ӥYuzw+0ޓYw3є]}Z&nEJ#%hq t ©,M!poI,z>q \%aHUِN\1e:w +Wp2h~۲Ez_M:%jM-%;q=gLJ6V8JЭYo;Ab(.v +X 4y7{%$Ag(.o&K@ +'^{Vyךߩ1ǧpL`", 2=#s鬥^X,=P:jh8é/.F4h7sVIՙiNYl?|Dpg*(L[&v{LhJݒwoRٸ8D XAF&.7;1!!E&:+PEJŋ0FBPVA>BaTu"Hm鸞"GP[:- UoC%O[*qԷnl={@{ٸ4. |]1)?G2G$3j{WE"^qҕ$=Pw' +nܳ2pMO[l߇қL!}tĘvLNNI];?or.׏@l)tzH@3 6 ya<IF?fYmn! 5g,n0V$]X"ljԻ]J̺FE 4|LjFo 6Bqo|$gdc^JCCT#GkA}2,3< ߦ9U~CʋSEQp5O=ef9$%^h)MZ#Y@-C.&tKnݍ;._J)W3>~}-3ErNd +B&t=E}TUv&H?8T.F|au?dzגiz5q' EƆ*9?psUboR5y 3uAo" hxX_ZsS9*IxS}э⽐Dnx 4JdDffjŘ^ůEba8R0}q}{udF{EvS D.)L,mt oPazYF?@aI4z;S3K;2xIEKsĤμvɃm%8c`zcrK*(/QB4~I6F#|=K7U$W\hr70mAh:Tj#oz\D7)"msRʑ,F קU}lFN +Uj2r$"y]=7p{,aD,eu%1h|t|cOItG`]T//[\tl:`aKE +!#& Ml ;V(Q}l#rXĵD2 "_Y/lm1=GJM]) l`BJO0PZlC!q+{wS͒let&t19gr<:F+E>̷O;IE[M2qDa%Jp.<93"֢?)PFI91+a">Hdwx5-7"5uهc @nL+}nCv3ɕY,AIpUշ[65ʉ1hYM k]:z-HEr' !e{ ӃbJbt ϥJbc1夲??K9b!<+Be?!Ut@cKBx>r5?W]6=A^:uaϧ`hfQ“[wte顿!ª0?F;+ źJ&Je# Eh: x!$7x~i:Mw+ V V]gf` X&^@c0hq+?S{ :4']U9)nF9MlvC0?oF}ȼDA'JK^_(?|HHoQ<lY,e\zY!񕏥c<qEDI,Mn\FTH_4`%`5!4/ቨSVLke_2c:vcUlIub!@ܭ(('y=b5^.53WҀ)'5qGQwb^ei#\áIB\K`\LDwCd5 gJKZQ1q; ++!ܭED%ITN(sg?uz1qBL_ x*@9DqVvIص sa3ӝ.ۨ0LDC'nq +U Km'a\DO@d1 `3"ʲT0hVɡHͶAƧ)@cޅ.t%ImGIòŋhT(.w HdxK 09r%2.,QQA\}C$SF'M0j\%L!?A]cJ(:$ڽ#ѕ]}4( , o]MPջe ɤ'WhgmNFiҌQmQM "@m1_o'k< +,6tyC6"i9 *:CZbCJ4 oP1{ɲ`_2 +<~~/-gfIX!RU -3[y4]聦[(y=Yӱ_'8٭Bx7PMlp4 7tfˌPg׭h8O0P͸ã҉@:7KEW!`pE֦M}YǏ׽p*Aa$; *+5EZ(K[q_2pRo.XGҢ֐SjȂ-GH є!^db\wyH#aAo +d0r+Ɲ}-#eদA))޳5QYQ-1Tέ.(ZN!$7iPxiI|u/|\U"l#*qrC8hcu5ě1SԔtSɻV*/{pI:7ܪJkenh"28wʅ&lErE,&y:!9zMƢle~y{\ $.#~S…Tۈ>ՉG!'4r^(4g--9Lv +X4K%@B:{+~q`uTeL۠[Lhhj 壈{EަIgq&Ձo/&~︥&KEj\ N$H1EEy:V4 >K9i}pdբAaNCٺtz%~%c,Bh! 1+@:s +{ @:A,*b]4LHR Tt_lca)202A>-ۋo$5R b[:EgC@`m}~e[TY5:ɏ +DaURӫ /D@/p۾=s)}c,j3*ix1㡆vUYSinT='GaoAf +(DW3e,Z{!sV6Q,(PWbR>ڢ(52q.L[ǩg- 59%xW[Am?TE2lP_gbl߶Jy{h%vQaxr1YaG0T1KGU@P֯[M@:n: +!mK<ڥ27b4yI L 9hWK9i_!Cˢcލ* YgQe_V%DqZV%(!tmU2K3yGdAvi((DD;56(wa0+ Fk~k861V6J愐nbsl0`ӝ!9V✾ #eU} +y! N?J +m!9Q>Wm4pJEJ h{@\;yد6o[j +'y;ȹXrrXBcjEu?L8  +UoXE%^Wngoʹ[\Wi#u" ZT[꨻ !ɯ(nzNfAi3:30z!P< pȰFw&U$XWigk8SXf T#J9StP.DlيN Y{6 +#NQz<=yΚ[(\#3c6#v(Σ^Z .K* *`لJ xh"x]!}[4ݡr" Ѽ!gM%9<`k"@4{/m7boꪢa}PttEPTT{A?gt4kp -`.:kܴY7Æfq>K{q\n`#BL\!F:V ++'YWCa4_jܮ +2{|_8H9H +uP/p[];V3GG-JN3uR]z6QξÙ(H:bh\Ӹ.gWwm88<6#З9G?c]lJ!kZ )̵+mWx{xA|zy]mp0.D8Ы߶hNzDx83nɏV֍hʉ8>;shqUTGDtѭfD~=nRm@]6 ~gfhBIXEor{i&*8rL;o)-n5uf0 xuHiǛA*6=ϱVv Z֕H5lv܁ GrJ[z6DPq!xhY !>%k9HDN,1R+z faB+yRi$ii% G¸ c4d%#fw~[q1:g%B +I .r eh\'1w; u˛WŨ1[9}æxz.3)GLa@oP*D!Ck"L8ob[tg0e E 5,s\ HQ]߶fͺU:X)jWD!c݌0uL]MR-܌?vTFi&[oMҎP(oR(""H3*q0]:PD ?<얕d\0(Ft53ö<_JZɻG9'?C#|lFO@MNUi+љ™H +[H}{~;LLF2t^a)*tmv*_ģ* &pBIVЮyēSPyi1S\] C862a4>^1‡n!Ee5vt~cEgtX =&؈z(5za d)3u4"؝fju4% C:n㎮.z&7C%m#~_c{wB+> ɖX4ɑuB)?ޑ]< C jײH.mldNo͙ eucEKo{S*~ͽ<"`tx.}II/wC3(ƏXw-.sq!.9r\)^Ϙ8@IN=WvemM*/a7zQZ3yO@%䦮H՞fG1'MH# ptr " QnN!OP;%V훠$05^McF7v7LsSAt`d&!Y9kbL.55▔lc5@da6ugF{Xw&cJ89j*Y2Ol,iwH$L5S^<({A>4DK1fQ(1j`WGhJ`wh<:HTC#l6\JD{Z8'y V.;/oLW7^]ڔXHXiobzPFEϻiWhAyC?09 :qTᕦ։!h:uI[ʞw/(ȉВ4&`W"l*gYZgEE+R_*}ۏ 3`h6'":hVOD`+4~q{@^[5G@4y T,F}T?ܨjH4G4&.\F!卌c%^J$dQIqzɽ @#absZmҲNԡPX) +Fbb<1Y½?ю0^ C7e{(8G~.CGjG,nεΞ 2շAO3n $_D 4hKnd{d)JRWف. M^!81`gr KV{48gcb$ [#.ĊIFˀ0W!"i$ +jqGky FLa[oG8:sGꂗtb=٪vm[Y9%2&˱gkH(r'M6CD ]ζl4 h1HF +2m$Բϸ.0*;-W B/*aJ%az#n!ʌgշ).`2ʍ$J,5Eٶ$@D Sw)\I!/,vP3/=55v ]viv K#3:RTCZcx'<#ދ}q#1HZȣxGoByKLmcvM 5f9'Dʨ .߉ĜE\}1VPwyaj60'J5xmH.YCd0}oꮟ2xziv&>甡OL&='KI3Vh-t:4mqM퍺-i̘:mв=cL `V ?q:mT_sYpgjKl? w<5 +_%,O8_f[_xN ku-ot2hej{IfENuBsRkw5o+ȷa³xU6%H cqbCA3>OA0CN7|{QN 9Rsӈ?T^\Z/Ufd(|TBW"ko! v-W5e@ 8z ] +Y#;Gw^Ө{cDh4< K'%(ݕ ox=-*S^c-\DByzsڒ(l1 cZeK0KʇPXZ|B`ح|+T3U8FFl˨ګ4nNBza'n*o C[g HMdªx + >Ov¦}1a h yEVӡ`!_%@+-[łv++Ʋ6#ll؈H0U^c+#Z\1ȁw,5 aG>r''\!0]j_`+ T>W7l䮖' nQ(F#]NL3TCd") sahvj\%` `ZnH|8J ՐЫPceg"axnh{)9J[gK +0q )z rXKC +4O%R/gpCR, zy,E[Bضc)кMY]Пǰ$33(lsDa3]iUԵDM(u%o.]" )Ҁ7!aR9*,I ilOhhJ꡹ùp[?CM79 ^xtJe7 ] H/*oB\Q|iy ZkGv\/ߵvq} +}|˽v|2U,xA=9N Pcj&ݒ%SǛH +WmnkS3GeZ3̓}2eɔ+7[hۮgw=xgK ɫ9Hkb_XtֿlW;Utj=)jk<,$udE5@. :DzUELN65kQ2aٸ +B4B4L6I+ Eh@5(ԯSk8LM{!,=]1V %+PWG+ Ӯ۳g'NggÛK_]rq}y͢ +[$j$!Q=j- +۝lAmW +@!ɍIE.%nqlےמtim~h8Ǡl#i;Jޮv=j2~ȉ~N% )Dz+s?4; Ig)cb2h6Bu:|J?ד5" 6ܲ^k@B%y%K٤|48/ͬl>?5+* ^Vt̪a2xVϲ Zfk$=\ {ZkW^\YU* _7_zٖ}b[,^+wLU"Xp# Cal0X<8}u7~"[̀K^@?A٧JRF;2#!"4Pc63K³~aY*KEQ\Y]3ZkLCBi>;0&F\dd7"o +^6޹k !U^ĎA1S c/&c2{i.Z- +pU흷rދھ2)Py /jNokYI%b>zRڹgT ڽŽ5,@;9̡ +̡1ki8Ù|ߘpj@k}遺96@IɾTbaףfx7<&r2-KT_Vf#p" ӉP s -zdG6$` 4,&ֱ2s@ZMȜ*M+g#KQB@9= cʙfw0n \"%/xa[5Zis*% +縏ny_r_zXv15$ ba;pԦb>ɻPX_uAҹ% Rrʄc4K]Q0:,hg8])kDݕ&>J`{xf?$^.@֎iٰBDZ^Z<5PNf2 5 +@P4 j#e.RJR|i0O0  9TѦj+ܡf|R/0;#Sdžh%bLiN0Żyb0f.s=Ɗ3^q.sEˋ;}D{8'fE pˣ{%-N#a|{HL[2=S,ޭHe T A 3 +Z5bX|RwS&\tw;tNHrgHD8Y(2DZ +bS 9Km%qp8Y KB,¡xiK˥\ݪ$>&͇T2&LWHxҘucG5Ғ!YQ]ۨX.ޅL ՙ#*B6kpf"g!zCU].tdvOJU u|:3Rbc&FT'Rq\S +D"z2EEvk&U>דb~IW Y%)28z~J4&Ux|6=2;/8>ȽG݇:3C}+E%="fv1\[FC6SO~t>g%EcXap/$$ٴ5uE_~O8&tQaׯ,W.QUUi/XЛ{TG&K1ȬbEܚ5Eae:J +-_}$fOydu|:Hog/[>'v\͍\¼FڛF2Ÿ4G64vlEfnLIVE6p U-E4_hn!2O?ݢ_{ͧJ_Ve{.͡bW:(,X{E9UST!j>j&Ttڕ}0fƽ\}V_SЌxL 5>Qu<Q/DC(p_R 2ؕNv+H07s$!WY`.Hp1X(sAt2MU PwE+:vilH]&lFG1;҅ք8:~2'9X8xW)|(֤\xU]Yce+]-~cez$h{UIvajvz/VqEn5lPW<jÅx0Vr9j؀"wD&U֒O9?8PJ866`DÆ>аA3̠䐀$*fPJ8 /Ppl6A2PA 7pA-|@d`A(0Bbg4,L@NAH0H'@P'0$|0B +C!P``>!00 + + h _W‘0C h fx  ,xa)x d ,6#@@0@XB +V bX@ V`H#X#X*`+`!0 @0@!` 0` `-@R  +$ F +$X|a@0 & x3R4lX! 0" @2B0B HX @H)h ȐF @ 2 4la`T   & @@0h@,0 p@ `m`<@HZCYH/(A@X@ v x@Q@ 40B-aCba|8(`H8p@N@X3FF % #`P&G ^p#%, /H`4 ASpLh #5 *` +xA (!P%t#h` + H`8@_A $h#H$N- B +@ + (P!X@C (@ PB@ )(@#D`" $0 3C CaFX!`J -,`#p`- ~ &)  Z(HP + +^ 0 +-p@ +f1 2T +-pVpB<,t  %)l @ "T- Π,8"!` L@U6 e X>p!(C 3 L(@Æ#7 HA Xx*P(¨  *BP(DT@&7 "@DA(  0L qF"0 +GB0#)!<R@2 HaH!Zp|@0 0E@ A^P`@D +ha  (L@J%@ J$ j #@(,C`apP +N@Æ@.@(A[0X X@ + (@_`"` ``A(A +r؅FTGBYxA/^a0p\ + VAL p 8 (e&pS +P8VP X #<@@@Y +```v#HS.T La "``:7[Jb Hb?а`,BT@H (! ( $8Dp $ 4 |F@,B +d AE #H&H , ,@@d0 $0#8#@0|aC0@ 1XxaG a3Gx t=Lc\F#0⒙'\.gj嚛p,2G:ʘXfh=7U `0ϥSp܊$TQfru~#YֳhF3(N8<51-+qfDX}R Vb^Z|qw~V1YcW +סH%֩wlMčCc㩤u0:Ω:SjpBKC(;L>+*rγ̃]=ĕ{P/Vfm\z({eIۜr\"3_P"JĤa !QR5G%Uo$q(>GT3c|NM*Z%{rH݊'!itQ|nuͬ0.;2*=/%Y,YA +I&1\'U1.l=)>]T>dzLG1 7&f-7[nb$sqVѼbհ)*;-91~ć)i(v|V&CWS$KQbfSҝThNDbgΊN= *ߊ~uV$\?){-:w +$&0GeV!D}2;7w,R =Fi8V;=J,ȣOIݱ;\ۺ?~g9Tq5YH%)OG9/H5C4NtEKmיVݎnrb ow5lxyUتޯ[WBD5Q*# 68XVAw8wU)\|_nNüW*[hyE|%ƌ-2=ݾG3wo]GTE8طG +pv4Q5ig6H6f|H~ȩ%IdJJsrvIb}4iN5/Nh~ȢPGkL%lu7!$3i-\IBCU,*{]JZ;PrKlsd6gvWRr]ӚYÄ[\Q6WWszH{mDa^)[αtU\kvƌ 8o^CrN Zt"BzRӼhtQW疺YsPjm5j=J^X5zqnNsgU3Q>ԯGhΡ۳ U[=Ƈt"\ؕ%Y;st6A?޺03mӞq`UFއnΌqpl9pӊ6֠L93+Vy1QS⡓'qHqNM]AH,G.JUѡ.5h:2dS:T^:5DV4:PMrʞ_qqie:s@#^j5CDe#Zܹg&ܡj7QPx ڱĂ)j +Y4FH_mJ/!nߵK] 0X=š֡J3Z|MhOͭk{. '{Ut;52SceCVue"rwViuazf'#{ŎMonSTTXs5a?O]{хL筋*bhf_)-ҕ5ѨPhˬf6Wb&P3Vj :xB5Z>92?nJhعBԋH]O쌌1=}̕~MC]^P"O܈2_sTm=Ar + )T_)rݷ%olX+4CW]]ÜPv]-ćlHlݤߥqxGCϿc\Vi@*;=3"sEHtۍ.2&b@gg3!9-lWnaspȥeIA:E9U#iR[&Ov(ήbȄJl{Fëb%7̌1w„2CUv3;)"#%똥SC1̦FNfhivbPIM1w y"3M)*_X%ҤL 4GQRevxU [TgH{LmXTwphCC) ÍnvswD|҈kE!α~f$0$."Xh:w^[柂DTBSyL] %48{rgϽa LSE /-@)8g .Ђ \!S&GN2&g\M=ctu4e=6?]~S;49 MDHCw6p)-pdt<`)a7%h0 X<4gΌ #)cG&jZT2Rd01dMGhJtT>3Iհ2+;*L6ry=ԵFdaPYSt [uc +m.bx?3mĈhg62+KRC6pΥ,(}^Tu=G;+/o74Eri/,XX͘+Z˦l&^a :&(>ċ[A%vƩ.aj(U\ +S>v5lP$. ET;z>PSxa +'692PE^ icr2iLmQ'1< ,z ʴf )2Sqw$o!VNc~J%ML(V$j?#G!v9V]x~8yOs4K`&cbW)g$<46@G',/vqc*G=MjF% +2kG+1B-3zO ;,; mCUb" ON&,l˙Qɓ%@4ybNj E:# }1%~ܴAŸ1xpTqOi2d6Jo`sՌf^85{E&NMc#uGDzfeaYs#V~ r#DNQJEl,K5l|+} +oH]_3aNYh [կEIxSχ6܀$S:}Q4:_B*DVIbrQ]O~Аhe7e!fN$UroBc(ηjbIB +6bVP~v"֐Kb-2KHqRʏ* +tL g|f%k'󋹕eeR#rM5&g5 +yoYi?ASbUbY3WЦ,U&UQ|2Hd N`9Ǭ^awUoRLnlL *}ݑa:=Viٱ1`>9ELʇQ(Ra潲)N{0 +N)AQ|h5Do$瘍?sglfwMT2T1j3쉍؎B1i=1wO;뉮L%Qn :;Ifxi_z4M}D(}T;֍QKst[ktמ*aT}9y1(~stQJOR1'NKqCK(}i9m_$7е^B?^=#C;?T"3GWr c.F]my; u*G/bY,ź轎E21 >3rT> {]\ÆʳG>[WO(u銄3ғ^ÆN\sdo#fEʡqڰ:N\[X^#iߕݛ.#>t9c܄Bl36c%[BJfD0OyS$=۪HIDz՘fa|y{H},ǞYL?#îjKr 'H3vL5$q{ZNTdOO{~!">^lT m ώAo%sq~5M#d)ɐD:6QTƘvVי56L2 + MT)͇"u%BO 1Gލŕ+s,FPh#K]xeukPIկ#wt]y"Vnc&Δ%KŔs/4QYtC7>\e٪aW뢌f%Hjl|7cIﳻN͋3JD 1DR./Wf1h,RBgf\Tcf =SmXU|Lg:IJ1j(|h;6)rWm^RUQUyu-[C6TKpа! 2b f! +\A 30 ^1Pa bh +\bԙf(; rفG>>"6ȆF+ՋN:q'u ]"XGzi i1Z,NVWW&_|=O!љ3]B=܅J.ҜS.o +e2J&W!+N&Lc +;3DO63!JwYilZ՟q+1ԹIE"m?z_jma(\"H5#j"+f,뚼,,p&wg$ML)˼19:Dņz\%B8+*HlovRH2SG+/H%Y{&E(GcwݜkQׄ9G7K;F:"")7cиqƿ$ZÒ:=M>LLuesI ",\إ"[M܇ibFI]NRt~C{=79WI/\,;݉ZܕX?2sSlS;4G42G]G6N\ޕIs/X5-4nPDNr4I`rCcc1Aߵ0vqϪcqGQ^,\+30 +{5NЍ.,sW!"ZYF_h4"eñG*l$RjeiU]c[8rqU}Fsb$?aMiaN/ф wD>ވ]E-V{27;9/oī"IdQ^f"1±!v)*;g¢X̔“^]EhT:uΙED?z(cr/ELtEHjn4ᜬD'3Q\s6իVePr618WHnĬ! X`<#hUjl!V;^H|5c"4f|UgoPbʭҭэհUIXV5luט'\tڹя®MWmdj"ٽ'X(Pb#*D':H&l#$[E*~j3Sk&n.sWw9U"ڑ\kƄ$+F~#gQmdGy*/NXe6۫r3 9<6kAU=2cH;X>*/6u{Ű1Y?ڔ&at0U٘Do!2[w2se2rCW;!D596ɞdC&I3)Ethܮ;Fc/S4=Gz/>^6%RcW* ijt`RZ{28xq,!n` oNi+{\޸ʻH34;2M̖)EV&uXdm0|X0>@B cDq5> A,- +1,DBϤN]{l}IP'lK ++-"VsE5,莾ɛ+4U}t`Y`+פ7yOľ1ȟpuŰ-iBL {=7>H'|9zTr_ro:/ ĝ$i,55Y}qFpS BFm$rOZ@N634--6*ԊBz"t,lc#TUSm⟌4#᪶qrWJo +MgQ[*O=h?/Zה>& (OwLdCҩO%4 vM(jqj3Sy]ĖzE(Fbeu[YO>p!Z\ɆM(cHt#q] Numo')~ +>#OP$jx_H>N}FvBROfWIJU3TzQ;UT[xL .bŏʅZ0}}y^I*^$ʬCi:nx0QFӅ[zq)v#\ +s?R? L@|`sp#Hu.-3&v!55m?R쯋MK7cX#0="; qOz׺ԣ0O2FB>3t +R@,!MM!zr4ɥwo`ט QqM7v:{;PӧyL#dxt&?Сʇ'=ώyRZ3y$镶p4)O$1f`-!|`jLLn^jzihm74WqLV}j0O¦rҷ3(M>H[G TXf黐Q' YW Z`(VW5A!AA:(Tj#"KSq]k'{`]!n7!@_pqlW-:+IVx86]1!p>GڗJ[;7G@(E@MS9M'ٛ]A +-p'zʉPj,: Z!ϐ47v;+ۇ GQ'h%:n8=7mfLKxy }쀜XaxF8aoG[KZn40{0ρ͇WHk"K*KT 9qOPa?A !T*:|~eZ)M `6uAb̡l͋m$n ^.]+]j쓨?S-/!㮣(ѧ.QwJ #ڕ@% ()Cדp0h9Nwd arNC̋yg[oF,x{V<:ڸnܡ{-lŮ o.1sr0E|v) >csk()ݞ +$,4SJNp ȰOZFr?N J4jL@%Ha|v0f=Rl队FWb%) KQqXRI&0PtQ]W͓\<'В\dB øZ!YH.I)O"hV00dR?8Vu Yy=~L_$0ab祔RRcy;9>ESlJl8>ccjCVNI2u`̋Juu? s&NBw =Cu[E @2J|D52HJ%" :bQ\F%v7?Q +1 +l9{)H #O>Ȥ߱_#C},Z& *3 +D8Q9`.n>зJUN(qLGzT:4aN9ov)"mĺȀH8ִ<'|}/bKu*%Eq^ZQ@B?ψ3\!)hnyCX GF#d5NoK4u_)|UsIx%U4GCP + "|.R[mu$FZէx+Ty/uEν.9O(˜~8wY΃t?d| %TZN~,YeԾ~hH.µ 8̌V"4|C*%N^B!ׄE1^D +kf`wynGMhڿ +Nە; V[GFvpJlǡ)ƣXdY!o=Xְ%4 MKzB wtusILSENuedlPs.E%4N ٪R';h2lèEayP&'u_،.pҎ̹Vm2b!Cz'|C>&ImQh-I<)\ۍx(v̿8g:e񚪖Tf@\n' lre|s*᜚ [ +h;E}֨b<\}&ZQxBri& +B] 6;pTu":|" ܢ..:!'3Afγ#Ugh ~ɳC*>S˝[_BzLdrXqo`$~H5R2b$HU;O;pkPFV._+A žo_`[q/B/DGt&عOmn@oOy!l)K=?gl\+7Y魅Pg HRV$$WLD;O<4:$\@XIF# -CG7^^`Fl}QvH7#8NVQ&Eށ.+oZ^xذUײ_v@٘Dᢱs%9l ACoZ&dt^ٜܵEFhz +8aġRb:Wo#XB\ƋmTJVN[Hэאҋ!EprqS"#2l1R![8+\Y%,)%5{9K])#ONV#:`#WdaEI`9lx0 =lfd^ "|25s؄mݮsdB |7+^\luʒٌW$7uO]@_tmmZG_^%p} ȌT{I--Rc}*8 +06N*IjZ +" 0rPIB~?ϫj8Jb((T(&mRMT.}.<<kotAZ6V9o 拳SQ{{yhO) eΗn‚dNXla3Mo 8OI6nt- rgg\ĞX.")-d*^dO9D+ccge1Z)ِ! 0? "hN2濉͙nnO&d^F+jsJ̨ aP1"*ż,^N̄>Le +6 +dc`oU/1mj)| +.P[¶p=P]HYt GDL] 3P c6H37i%GZY'WJ-V; #pMW`a4 Q<Η̷-y&qEHh/WX\,&sJVj% $B<B\ +=[ٺ68DgS!Zeژ PͶCC D{B7R%참n**! +v$sK][6w* ooԙR1.DQ˫G\)(tBd & 5-pBTCu0][`@hǡ"paEWc0NRۓSJ8 ŦhF~4lD)rv!tݕeNTLYL{@@¢˖k&OeyCVX#1VO&l9i^]j6$=!anI#N8Ӏ֐n3֜KlLWmfh0,0AAs9=c7L{Bq.w!פD}MADBP 烯PҲ90#%6"Va1.MryJ4O)kQz4eug?F52j^2͢Rw@-L"@L;V bP1?$k,c`Y, wޤw[ė=m=$)%J+h؁JFaUbRL< +K::nOT5d.$[,)RR3 +*b m.,46S`CE#u :QDԹa%W"c6 d IŜP/BJ*^s+[Oх +jRx (]kܐPjFi)*EFa6HaMNvy%1oUv$:yl IpM\9,w)_ZG3#pMFZFa +Us=0AwQ&bs2N;w?M?2O/{Cys G#m&_B%rvtGP 8&&!'_t~(G{?/a?AunH5kN,E\訽(_ #7}/k@zh4at.!s[;9'+I7ߠ#aVE" + dt%3ܫ+6RG{g(΁#< ՃN8J"U KѱRK7qU1^Ég!g Gt\(4-eP05'}/@+溜S7Gn/K/TNŰDw +0P\˿[BEDi)5HPw_{礱̙ +ՊMS·b@7JG4SYuݤk $U4jC-CM:d4>%O L`NpYI~Ap˭c /E+־ ;ӽx%2cEz#ao3 S k&~{9˺ci2|kG(1 +J㊌;ɵnG!5xR52IyXY@ƍ7Nq +?^viH5"B-[/"iJֽ9J8!vHV +u7S J툭8WSJ~ +/Vu D"(8nrxY5RYcU72/(D )d +06Z`5Z1b,Vt FF7kM4ĪܾcbЪ/QUs+bFRϜO)W)H7K0ItPC}ڴ)9}rS)\ЪlEo&i.C;38K^!z̹i4Y!dX 9%L92琊||F:94fwG:𭸻@4׬#iɎ[k$'|ZQ& 4ĽqHDLpBL8qTm=EN=C}Aj\@c%~C//<^]S-m3#yVp`V{.* ~[Eҷ#aBKL5~K x3ްoFdr^\姳MCXuDLyDC[4;eOD ;Y\vc]Oᡧ gBT~Mgd=C%M`Y7XzmvCӝz +'*dS xRW뾭W^!7*&U#Y6]AFAio:BBX jQPkn >P$rfՍ^Ә9R<UmznHTRntmDPI> L4mڸrY?Nw_̷k{ *c*qY4Isdr4nfY%a~R3@z_dtS_m1ʇs 9Gi{һ=gEl1}\΃X1]G@-]7i^zj!r6rEĄ%Nb91S$FY w\H>V58|0F7ށaM%|W&#ƀ1gg uYs.a+ۀW*?z9['>0c{S1jO1;Ȝ㦜"@lQ'lX͠rz.*sv H6 J&2sQ$"~6O;j&sDy+S].Z,"fPz s)r|QދZ52Pw 31,w%z#*4O#F%xcNlR'AuѤ7=9 +)\a cWF/@ t5_9rR¾H\SeɬkHE;tϘCY=7^*f 6EU0mK^]W"DZ+{F-}f3q!eH i ZȬo4 T҄@`TGIɀI(5 5r< odCGV}-B ɽc=$ *$_#7 ى +R`DdH~؁QELFv"(F:W Hoi7ACODCGyE(4fdq/˕EVN8ҾdWY(*PmccP2KX088X`BĘMAQcfr#}F^qu&Dn,y)Oo*rK<ޞ5QcJ},?=_HnD' _@ހvz*5c V(&[6~zjKzb72<>*Z¤;܏"gA"~;"YlHe9P,!C))|A"4 /q!+'6T&{'6qGMX(l鮪'MG8LKСX_{P p/dNU A7=͚h\_g_'ba/f9sl,ޯ!]J] <5 - ӂeV=SRQ/Se{@8KE3c0lR +\+L֗m&y{B9xr͍xeAe/ f4 zD=貣 z/X?rR|:6?ir |,}<d7>_fڞp>`L Ӆ8/G\1+#6dW( =<VT~8Clk[)PJ%nG29Tn=^znY%Yk,KC^rvR5axs ׍;Ao] 0P E=7@hV,ὶL0 x/l҅{3#p]4צB7KB_7koxRQ)& L=Bg5#JIbp<W+2z2berC ,aVi[:(:Qœ =ڗ,Nf[%[C7rZirP}jrIOSfM%dxĢ_Pb/ \ (;UZ^f <1g@E(`DC{'Y=[JxZ"Ũ`8srW8s{9Α@p9sT= OwQ+/E3@'I-+QaFJgS#.-8sq} +ʶ F|b=&˥l=@Ok'CA-|LSrwsPnbL#.گRT$~@/Ya.rڿ,q9*%T.k}Zdm/!P'<؎toF ̰d +f,G)h& +pu(9ʳ/n* /V)%TPX:xB3Mg1R# /0OéJR$ OO+Qf{_۩e*T$H +JWTZa5!Fj卺&Id9A}9OZ):Tf+)z*%مTKyPNp9C[)oО7+ #vCPB +#ddc< pYottMs8 +'&Z^W`i՜> L rDCX\ju8%`Xay1JsRvJ/ [$TK^(<L _kocGl~5b `DK?澑XaŸ _Խ/ZZ=$4Up1!lxPof +endstream endobj 41 0 obj [/Indexed/DeviceRGB 255 42 0 R] endobj 42 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> +endstream endobj 37 0 obj <>/Shading<>>>/Subtype/Form>>stream +q +423.581 343.205 m +411.447 342.156 387.177 340.633 v +372.152 339.69 358.465 338.792 344.894 336 c +336.717 334.318 330.724 332.681 330.723 332.681 c +324.75 331.049 322.99 330.27 315.457 328.217 c +309.631 326.63 306.644 325.825 303.023 325.267 c +301.176 324.982 296.288 324.301 290.043 324.426 c +285.441 324.517 276.671 324.763 266.847 328.472 c +257.471 332.012 251.538 337.044 246.304 341.565 c +241.87 345.395 229.321 356.469 223.051 373.325 c +220.65 379.777 219.317 386.682 218.999 388.388 c +218.101 393.207 217.481 396.532 217.533 401.021 c +217.562 403.555 217.761 413.618 222.851 421.787 c +224.733 424.807 226.727 426.932 226.727 426.932 c +228.951 429.302 232.057 431.822 232.057 431.822 c +229.661 428.784 226.307 423.81 224.17 417.021 c +217.608 396.177 228.541 377.371 230.723 373.617 c +236.288 364.045 243.943 357.335 247.064 354.638 c +248.467 353.426 251.433 350.945 254.894 348.638 c +268.988 339.245 284.16 337.508 288.851 337.021 c +309.289 334.899 325.713 341.07 333.915 343.66 c +352.408 349.499 381.018 353.525 423.581 343.205 c +W n +q +0 g +/GS0 gs +0.1420344 -114.832222 -114.832222 -0.1420344 320.4740601 437.8004761 cm +BX /Sh0 sh EX Q +Q + +endstream endobj 38 0 obj <>>>/Subtype/Form>>stream +0.047 0.388 0.416 rg +/GS0 gs +q 1 0 0 1 222.9685 373.5923 cm +0 0 m +-2.9 9.454 -5.63 19.137 -5.432 29.02 c +-5.232 38.997 -1.622 49.361 6.208 55.546 c +8.473 57.335 11.388 59.944 10.005 62.477 c +-8.33 54.574 -23.297 39.169 -30.668 20.615 c +-31.361 18.872 -32.109 16.959 -33.751 16.053 c +-34.701 15.528 -35.819 15.425 -36.9 15.326 c +-63.379 12.895 -89.503 5.967 -113.395 -5.703 c +-119.202 -8.539 -124.898 -11.739 -129.948 -15.799 c +-134.02 -19.072 -140.901 -25.075 -141.67 -30.455 c +-139.558 -28.031 -136.724 -26.606 -134.097 -24.837 c +-131 -22.751 -127.831 -20.772 -124.599 -18.903 c +-118.136 -15.163 -111.421 -11.861 -104.52 -9.009 c +-90.709 -3.301 -76.155 0.604 -61.346 2.605 c +-40.922 5.366 -19.983 4.501 0.082 -0.267 c +0.055 -0.178 0.027 -0.089 0 0 c +f +Q + +endstream endobj 39 0 obj <>/Shading<>>>/Subtype/Form>>stream +q +273.822 297.63 m +280.009 305.292 293.447 314.809 v +306.573 324.105 315.457 328.217 y +310.949 326.765 304.705 325.194 297.142 324.612 c +293.358 324.32 285.958 323.801 277.142 325.614 c +263.721 328.373 254.694 334.958 249.532 338.809 c +242.714 343.894 233.293 352.607 226.35 365.94 c +224.293 369.892 220.402 378.187 218.553 389.362 c +217.498 395.741 216.473 402.345 218.468 410.553 c +220.053 417.077 222.901 421.961 225.017 425.006 c +223.722 423.933 222.243 422.602 220.654 421.015 c +219.31 419.847 217.432 418.026 215.625 415.49 c +202.969 397.721 212.705 371.384 216.851 360.17 c +229.996 324.615 260.504 305.074 273.822 297.63 c +W n +q +0 g +/GS0 gs +0.1683987 -136.1473083 -136.1473083 -0.1683987 262.3182983 432.0107727 cm +BX /Sh0 sh EX Q +Q + +endstream endobj 45 0 obj <> endobj 36 0 obj <> endobj 46 0 obj <> endobj 47 0 obj <> endobj 26 0 obj <> endobj 44 0 obj <> endobj 43 0 obj <> endobj 30 0 obj <> endobj 31 0 obj <> endobj 32 0 obj <> endobj 33 0 obj <> endobj 34 0 obj <> endobj 35 0 obj <> endobj 53 0 obj <> endobj 54 0 obj <> endobj 52 0 obj <> endobj 55 0 obj <> endobj 51 0 obj <> endobj 56 0 obj <> endobj 50 0 obj <> endobj 57 0 obj <> endobj 49 0 obj <> endobj 58 0 obj <> endobj 59 0 obj <> endobj 48 0 obj <> endobj 60 0 obj <> endobj 19 0 obj <> endobj 20 0 obj <> endobj 21 0 obj <> endobj 22 0 obj <> endobj 67 0 obj [/View/Design] endobj 68 0 obj <>>> endobj 65 0 obj [/View/Design] endobj 66 0 obj <>>> endobj 63 0 obj [/View/Design] endobj 64 0 obj <>>> endobj 61 0 obj [/View/Design] endobj 62 0 obj <>>> endobj 27 0 obj <> endobj 28 0 obj <> endobj 29 0 obj <> endobj 23 0 obj [22 0 R 21 0 R 20 0 R 19 0 R] endobj 69 0 obj <> endobj xref +0 70 +0000000004 65535 f +0000000016 00000 n +0000000189 00000 n +0000029207 00000 n +0000000000 00000 f +0000029258 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000042379 00000 n +0000042452 00000 n +0000042593 00000 n +0000044202 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000106802 00000 n +0000106871 00000 n +0000106943 00000 n +0000107015 00000 n +0000107916 00000 n +0000029836 00000 n +0000041112 00000 n +0000104227 00000 n +0000107547 00000 n +0000107670 00000 n +0000107793 00000 n +0000104466 00000 n +0000104612 00000 n +0000104758 00000 n +0000104904 00000 n +0000105050 00000 n +0000105196 00000 n +0000103860 00000 n +0000100553 00000 n +0000101908 00000 n +0000102849 00000 n +0000041177 00000 n +0000099991 00000 n +0000100039 00000 n +0000104403 00000 n +0000104340 00000 n +0000103797 00000 n +0000104006 00000 n +0000104101 00000 n +0000106586 00000 n +0000106220 00000 n +0000106000 00000 n +0000105779 00000 n +0000105560 00000 n +0000105342 00000 n +0000105437 00000 n +0000105655 00000 n +0000105874 00000 n +0000106095 00000 n +0000106337 00000 n +0000106461 00000 n +0000106681 00000 n +0000107431 00000 n +0000107462 00000 n +0000107315 00000 n +0000107346 00000 n +0000107199 00000 n +0000107230 00000 n +0000107083 00000 n +0000107114 00000 n +0000107962 00000 n +trailer +<<8ABBDF602B66544081F9CA52E63605FB>]>> +startxref +108155 +%%EOF diff --git a/resources/Telegrator_logo.svg b/resources/Telegrator_logo.svg new file mode 100644 index 0000000..ceae88d --- /dev/null +++ b/resources/Telegrator_logo.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/Telegrator_nuget.png b/resources/Telegrator_nuget.png new file mode 100644 index 0000000..22fec0a Binary files /dev/null and b/resources/Telegrator_nuget.png differ