Добавьте файлы проекта.
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
[*.{cs,vb}]
|
||||||
|
|
||||||
|
# IDE0305: Упростите инициализацию коллекции
|
||||||
|
dotnet_style_prefer_collection_expression = never
|
||||||
@@ -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
|
||||||
+363
@@ -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
|
||||||
@@ -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<T>` (which I'll call `container` for simplicity) to change the user's state, e.g., `container.ForwardEnumState<YourEnum>()`.
|
||||||
|
|
||||||
|
#### 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>(QuizState.Start)]
|
||||||
|
public class StartQuizHandler : MessageHandler
|
||||||
|
{
|
||||||
|
public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
// Create the state for the user and move it to the first question.
|
||||||
|
container.ForwardNumericState<QuizState>(); // 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>(QuizState.ExpectingAnswer1)]
|
||||||
|
public class Answer1Handler : MessageHandler
|
||||||
|
{
|
||||||
|
public override async Task Execute(IAbstractHandlerContainer<Message> 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<QuizState>(); // 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>(QuizState.ExpectingAnswer2)]
|
||||||
|
public class Answer2Handler : MessageHandler
|
||||||
|
{
|
||||||
|
public override async Task Execute(IAbstractHandlerContainer<Message> 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<QuizState>();
|
||||||
|
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<QuizState>()`**: 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<QuizState>()`**: This moves the user's state to the next value in the enum sequence.
|
||||||
|
- **`container.DeleteEnumState<QuizState>()`**: 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<Message> 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 "<YOUR_BOT_TOKEN>" with your actual bot token.
|
||||||
|
var bot = new ReactiveClient("<YOUR_BOT_TOKEN>");
|
||||||
|
|
||||||
|
// Automatically discover and add all **public** handlers from the current assembly.
|
||||||
|
bot.Handlers.CollectHandlersDomainWide();
|
||||||
|
|
||||||
|
// Or, add a specific handler manually:
|
||||||
|
// bot.Handlers.AddHandler<StartHandler>();
|
||||||
|
|
||||||
|
// 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<Message> 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<Message> 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<Message> 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<Message> 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<Message> 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<T>`. 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<Message>
|
||||||
|
{
|
||||||
|
private readonly long _adminId;
|
||||||
|
|
||||||
|
public AdminOnlyAttribute(long adminId)
|
||||||
|
{
|
||||||
|
_adminId = adminId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Message? GetFilterringTarget(Update update) => update.Message;
|
||||||
|
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> 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<MyHandler> _logger;
|
||||||
|
|
||||||
|
// Dependencies are injected automatically.
|
||||||
|
public MyHandler(IMyService myService, ILogger<MyHandler> logger)
|
||||||
|
{
|
||||||
|
_myService = myService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Execute(IAbstractHandlerContainer<Message> 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<Message> 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<MyHandler>()`. 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<T>` 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!**
|
||||||
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
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.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
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
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Telegram.Reactive
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> **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<Message> container, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
await Reply("Hello, world!", cancellationToken: cancellation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration and launch:
|
||||||
|
var bot = new ReactiveClient("<YOUR_BOT_TOKEN>");
|
||||||
|
bot.Handlers.AddHandler<HelloHandler>();
|
||||||
|
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<Message> container, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
await Responce("Welcome!", cancellationToken: cancellation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration:
|
||||||
|
bot.Handlers.AddHandler<StartCommandHandler>();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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<Message> container, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
container.CreateNumericState();
|
||||||
|
container.ForwardNumericState();
|
||||||
|
await Reply("first state moved (1)", cancellationToken: cancellation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration:
|
||||||
|
bot.Handlers.AddHandler<StateKeepFirst>();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏆 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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; Shipped analyzer releases
|
||||||
|
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.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
|
||||||
@@ -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<ImmutableArray<HandlerDeclarationModel>> 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<AttributeSyntax> 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<HandlerDeclarationModel> handlers)
|
||||||
|
{
|
||||||
|
StringBuilder sourceBuilder = new StringBuilder();
|
||||||
|
List<string> 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<AttributeSyntax> GetHandlerAttributes(this ClassDeclarationSyntax classSyntax)
|
||||||
|
{
|
||||||
|
IEnumerable<AttributeSyntax> 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Telegrator.Analyzers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a target is not found during code generation.
|
||||||
|
/// </summary>
|
||||||
|
internal class TargteterNotFoundException() : Exception() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a base class type is not found during code generation.
|
||||||
|
/// </summary>
|
||||||
|
internal class BaseClassTypeNotFoundException() : Exception() { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
|
||||||
|
namespace Telegrator.Analyzers
|
||||||
|
{
|
||||||
|
internal class HandlerDeclarationModel(ClassDeclarationSyntax classDeclaration, IEnumerable<AttributeSyntax> handlerAttributes, BaseTypeSyntax? baseType)
|
||||||
|
{
|
||||||
|
public ClassDeclarationSyntax ClassDeclaration { get; } = classDeclaration;
|
||||||
|
public IEnumerable<AttributeSyntax> HandlerAttributes { get; } = handlerAttributes;
|
||||||
|
public BaseTypeSyntax? BaseType { get; } = baseType;
|
||||||
|
public bool HasAttributes => HandlerAttributes.Any();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||||
|
<Configurations>Debug;Release;AnalyzersDebug</Configurations>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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<TResult> WhereCast<TResult>(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<TSource> IntersectBy<TSource, TValue>(this IEnumerable<TSource> first, IEnumerable<TValue> second, Func<TSource, TValue> selector)
|
||||||
|
{
|
||||||
|
foreach (TSource item in first)
|
||||||
|
{
|
||||||
|
TValue value = selector(item);
|
||||||
|
if (second.Contains(value))
|
||||||
|
yield return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IList<TValue> UnionAdd<TValue>(this IList<TValue> source, IEnumerable<TValue> toUnion)
|
||||||
|
{
|
||||||
|
foreach (TValue toUnionValue in toUnion)
|
||||||
|
{
|
||||||
|
if (!source.Contains(toUnionValue, EqualityComparer<TValue>.Default))
|
||||||
|
source.Add(toUnionValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Source Generator для автоматической генерации Markdown-документации по публичному API Telegrator.
|
||||||
|
/// </summary>
|
||||||
|
[Generator]
|
||||||
|
public class ApiMarkdownGenerator : IIncrementalGenerator
|
||||||
|
{
|
||||||
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
|
{
|
||||||
|
IncrementalValueProvider<ImmutableArray<BaseTypeDeclarationSyntax>> 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<BaseTypeDeclarationSyntax> typeDecls = source.Left;
|
||||||
|
Compilation compilation = source.Right;
|
||||||
|
string markdown = GenerateMarkdown(typeDecls, compilation);
|
||||||
|
spc.AddSource("TelegramReactive_Api.md", markdown);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateMarkdown(IReadOnlyList<BaseTypeDeclarationSyntax> 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<INamedTypeSymbol> publicTypes = typeDecls
|
||||||
|
.Select(type => type.TryGetNamedType(compilation))
|
||||||
|
.Where(symbol => symbol != null)
|
||||||
|
.Where(symbol => symbol.DeclaredAccessibility == Accessibility.Public);
|
||||||
|
|
||||||
|
// Grouping by namespace
|
||||||
|
IOrderedEnumerable<IGrouping<string, INamedTypeSymbol>> namespaces = publicTypes
|
||||||
|
.GroupBy(t => t.ContainingNamespace.ToDisplayString())
|
||||||
|
.OrderBy(g => g.Key);
|
||||||
|
|
||||||
|
foreach (IGrouping<string, INamedTypeSymbol> 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<IMethodSymbol> 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<Type1, T>(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<IFieldSymbol>().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<IPropertySymbol> props = type
|
||||||
|
.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.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<IMethodSymbol> methods = type
|
||||||
|
.GetMembers()
|
||||||
|
.OfType<IMethodSymbol>()
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Telegrator.Generators
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a target is not found during code generation.
|
||||||
|
/// </summary>
|
||||||
|
internal class TargteterNotFoundException() : Exception() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a base class type is not found during code generation.
|
||||||
|
/// </summary>
|
||||||
|
internal class BaseClassTypeNotFoundException() : Exception() { }
|
||||||
|
}
|
||||||
@@ -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<ImmutableArray<ClassDeclarationSyntax>> 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<ClassDeclarationSyntax> declarations)
|
||||||
|
{
|
||||||
|
StringBuilder source = new StringBuilder();
|
||||||
|
Dictionary<string, string> targeters = [];
|
||||||
|
List<string> 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<ClassDeclarationSyntax> 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<string, string> targeters)
|
||||||
|
{
|
||||||
|
IEnumerable<MethodDeclarationSyntax> methods = classDeclaration.Members.OfType<MethodDeclarationSyntax>();
|
||||||
|
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<ConstructorDeclarationSyntax>())
|
||||||
|
{
|
||||||
|
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<ParameterSyntax> parameters, SeparatedSyntaxList<ArgumentSyntax> arguments)
|
||||||
|
{
|
||||||
|
if (filterName == "ChatType")
|
||||||
|
filterName = "InChatType"; // Because it conflicting
|
||||||
|
|
||||||
|
sourceBuilder
|
||||||
|
.Append("\t\t/// <summary>").AppendLine()
|
||||||
|
.Append("\t\t/// Adds ").Append(filterName).Append(" filter to implicit handler").AppendLine()
|
||||||
|
.Append("\t\t/// </summary>").AppendLine();
|
||||||
|
|
||||||
|
sourceBuilder.Append("\t\tpublic static TBuilder ").Append(filterName).Append("<TBuilder>(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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||||
|
<Configurations>Debug;Release;AnalyzersDebug</Configurations>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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<TValue> UnionAdd<TValue>(this IList<TValue> source, IEnumerable<TValue> toUnion)
|
||||||
|
{
|
||||||
|
foreach (TValue toUnionValue in toUnion)
|
||||||
|
{
|
||||||
|
if (!source.Contains(toUnionValue, EqualityComparer<TValue>.Default))
|
||||||
|
source.Add(toUnionValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Telegrator.Hosting.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Hosting.Web.Components
|
||||||
|
{
|
||||||
|
public interface ITelegramBotWebHost : ITelegramBotHost//, IEndpointRouteBuilder
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<TelegramBotWebOptions> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Telegrator.Hosting.Components;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace Telegrator.Hosting.Web
|
||||||
|
{
|
||||||
|
public class TelegramBotWebHost //: ITelegramBotWebHost
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Telegrator.Hosting.Components;
|
||||||
|
using Telegrator.Hosting.Web.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Hosting.Web
|
||||||
|
{
|
||||||
|
public class TelegramBotWebHostBuilder //: ITelegramBotHostBuilder
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Telegrator.Configuration;
|
||||||
|
|
||||||
|
namespace Telegrator.Hosting.Web
|
||||||
|
{
|
||||||
|
public class TelegramBotWebOptions : TelegramBotOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets uri for webhook update receiving
|
||||||
|
/// </summary>
|
||||||
|
public required string WebhookUri { get; set; }
|
||||||
|
|
||||||
|
public required string WebhookPattern { get; set; }
|
||||||
|
|
||||||
|
public int MaxConnections { get; set; }
|
||||||
|
|
||||||
|
public bool DropPendingUpdates { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Telegrator.Hosting\Telegrator.Hosting.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="2.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -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<ITelegramBotClient>("tgwebhook").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory);
|
||||||
|
services.AddHostedService<HostedUpdateWebhooker>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ITelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider)
|
||||||
|
=> new TelegramBotClient(provider.GetRequiredService<IOptions<TelegramBotClientOptions>>().Value, httpClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Telegrator.Hosting.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPreBuildingRoutine
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the pre-building routine on the specified host builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hostBuilder">The host builder to configure.</param>
|
||||||
|
public static abstract void PreBuildingRoutine(TelegramBotHostBuilder hostBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Telegrator;
|
||||||
|
|
||||||
|
namespace Telegrator.Hosting.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for Telegram bot hosts.
|
||||||
|
/// Combines host application capabilities with reactive Telegram bot functionality.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITelegramBotHost : IHost, IReactiveTelegramBot
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Telegrator.MadiatorCore;
|
||||||
|
|
||||||
|
namespace Telegrator.Hosting.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for building Telegram bot hosts with dependency injection support.
|
||||||
|
/// Combines host application building capabilities with handler collection functionality.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITelegramBotHostBuilder : ICollectingProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the set of key/value configuration properties.
|
||||||
|
/// </summary>
|
||||||
|
IConfigurationManager Configuration { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of logging providers for the application to compose. This is useful for adding new logging providers.
|
||||||
|
/// </summary>
|
||||||
|
ILoggingBuilder Logging { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of services for the application to compose. This is useful for adding user provided or framework provided services.
|
||||||
|
/// </summary>
|
||||||
|
IServiceCollection Services { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Telegrator.Hosting.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base class for configuring options from configuration sources.
|
||||||
|
/// Provides a proxy pattern for binding configuration to strongly-typed options classes.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TOptions">The type of options to configure.</typeparam>
|
||||||
|
public abstract class ConfigureOptionsProxy<TOptions> where TOptions : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the options using the default configuration section.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection to configure.</param>
|
||||||
|
/// <param name="configuration">The configuration source.</param>
|
||||||
|
public void Configure(IServiceCollection services, IConfiguration configuration)
|
||||||
|
=> Configure(services, Options.DefaultName, configuration, null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the options using a named configuration section.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection to configure.</param>
|
||||||
|
/// <param name="name">The name of the configuration section.</param>
|
||||||
|
/// <param name="configuration">The configuration source.</param>
|
||||||
|
public void Configure(IServiceCollection services, string? name, IConfiguration configuration)
|
||||||
|
=> Configure(services, name, configuration, null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the options using a named configuration section with custom binder options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection to configure.</param>
|
||||||
|
/// <param name="name">The name of the configuration section.</param>
|
||||||
|
/// <param name="configuration">The configuration source.</param>
|
||||||
|
/// <param name="configureBinder">Optional action to configure the binder options.</param>
|
||||||
|
public void Configure(IServiceCollection services, string? name, IConfiguration configuration, Action<BinderOptions>? configureBinder)
|
||||||
|
{
|
||||||
|
var namedConfigure = new NamedConfigureFromConfigurationOptions<ConfigureOptionsProxy<TOptions>>(name, configuration, configureBinder);
|
||||||
|
namedConfigure.Configure(name, this);
|
||||||
|
|
||||||
|
services.AddOptions();
|
||||||
|
services.AddSingleton(Options.Create(Realize()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the actual options instance from the configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The configured options instance.</returns>
|
||||||
|
protected abstract TOptions Realize();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Telegram.Bot;
|
||||||
|
|
||||||
|
namespace Telegrator.Hosting.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Internal proxy class for configuring Telegram bot client options from configuration.
|
||||||
|
/// Extends ConfigureOptionsProxy to provide specific configuration for Telegram bot client options.
|
||||||
|
/// </summary>
|
||||||
|
internal class TelegramBotClientOptionsProxy : ConfigureOptionsProxy<TelegramBotClientOptions>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the bot token.
|
||||||
|
/// </summary>
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the base URL for the bot API.
|
||||||
|
/// </summary>
|
||||||
|
public string? BaseUrl { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether to use the test environment.
|
||||||
|
/// </summary>
|
||||||
|
public bool UseTestEnvironment { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the retry threshold in seconds.
|
||||||
|
/// </summary>
|
||||||
|
public int RetryThreshold { get; set; } = 60;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the number of retry attempts.
|
||||||
|
/// </summary>
|
||||||
|
public int RetryCount { get; set; } = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a TelegramBotClientOptions instance from the proxy configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The configured TelegramBotClientOptions instance.</returns>
|
||||||
|
protected override TelegramBotClientOptions Realize() => new TelegramBotClientOptions(Token, BaseUrl, UseTestEnvironment)
|
||||||
|
{
|
||||||
|
RetryCount = RetryCount,
|
||||||
|
RetryThreshold = RetryThreshold
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")]
|
||||||
@@ -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<TelegramBotOptions> options, ILogger<HostUpdateHandlersPool> logger)
|
||||||
|
: UpdateHandlersPool(options.Value, options.Value.GlobalCancellationToken)
|
||||||
|
{
|
||||||
|
private readonly ILogger<HostUpdateHandlersPool> _logger = logger;
|
||||||
|
|
||||||
|
protected override async Task ExecuteHandlerWrapper(DescribedHandlerInfo enqueuedHandler)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Handler \"{0}\" has entered execution pool", enqueuedHandler.DisplayString);
|
||||||
|
await base.ExecuteHandlerWrapper(enqueuedHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<HostUpdateRouter> Logger;
|
||||||
|
|
||||||
|
public HostUpdateRouter(IHandlersProvider handlersProvider, IAwaitingProvider awaitingProvider, IOptions<TelegramBotOptions> options, IUpdateHandlersPool handlersPool, ILogger<HostUpdateRouter> 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<HostUpdateRouter> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ReceiverOptions> options, ILogger<HostedUpdateReceiver> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<TelegramBotOptions> options, ITelegramBotInfo botInfo, ILogger<HostAwaitingProvider> logger) : AwaitingProvider(options.Value, botInfo)
|
||||||
|
{
|
||||||
|
public override IEnumerable<DescribedHandlerInfo> GetHandlers(IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
IEnumerable<DescribedHandlerInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Action<TelegramBotHostBuilder>> 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<TelegramBotHostBuilder> routineDelegate = methodInfo.CreateDelegate<Action<TelegramBotHostBuilder>>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<HostHandlersProvider> Logger;
|
||||||
|
|
||||||
|
public HostHandlersProvider(IHandlersCollection handlers, IOptions<TelegramBotOptions> options, ITelegramBotInfo botInfo, IServiceProvider serviceProvider, ILogger<HostHandlersProvider> logger)
|
||||||
|
: base(handlers, options.Value, botInfo)
|
||||||
|
{
|
||||||
|
Services = serviceProvider;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<DescribedHandlerInfo> GetHandlers(IUpdateRouter updateRouter, ITelegramBotClient client, Update update, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
IEnumerable<DescribedHandlerInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<TelegramBotHost> _logger;
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IServiceProvider Services => _innerHost.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IUpdateRouter UpdateRouter => _updateRouter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This application's logger
|
||||||
|
/// </summary>
|
||||||
|
public ILogger<TelegramBotHost> Logger => _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TelegramBotHost"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">The service provider.</param>
|
||||||
|
internal TelegramBotHost(HostApplicationBuilder hostApplicationBuilder, HostHandlersCollection handlers)
|
||||||
|
{
|
||||||
|
RegisterHostServices(hostApplicationBuilder, handlers);
|
||||||
|
_innerHost = hostApplicationBuilder.Build();
|
||||||
|
|
||||||
|
_updateRouter = Services.GetRequiredService<IUpdateRouter>();
|
||||||
|
_logger = Services.GetRequiredService<ILogger<TelegramBotHost>>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _innerHost.StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _innerHost.StopAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes the host.
|
||||||
|
/// </summary>
|
||||||
|
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<IHost>();
|
||||||
|
//hostApplicationBuilder.Services.AddSingleton<IHost>(this);
|
||||||
|
|
||||||
|
hostApplicationBuilder.Services.AddSingleton<ITelegramBotHost>(this);
|
||||||
|
hostApplicationBuilder.Services.AddSingleton<IReactiveTelegramBot>(this);
|
||||||
|
hostApplicationBuilder.Services.AddSingleton<IHandlersCollection>(handlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IHandlersCollection Handlers => _handlers;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IServiceCollection Services => _innerBuilder.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IConfigurationManager Configuration => _innerBuilder.Configuration;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ILoggingBuilder Logging => _innerBuilder.Logging;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IHostEnvironment Environment => _innerBuilder.Environment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TelegramBotHostBuilder"/> class.
|
||||||
|
/// </summary>
|
||||||
|
internal TelegramBotHostBuilder(TelegramBotHostBuilderSettings? settings = null)
|
||||||
|
{
|
||||||
|
_settings = settings ?? new TelegramBotHostBuilderSettings();
|
||||||
|
_innerBuilder = new HostApplicationBuilder(settings?.ToApplicationBuilderSettings());
|
||||||
|
_handlers = new HostHandlersCollection(Services, _settings);
|
||||||
|
|
||||||
|
Services.Configure<TelegramBotOptions>(Configuration.GetSection(nameof(TelegramBotOptions)));
|
||||||
|
Services.Configure<ReceiverOptions>(Configuration.GetSection(nameof(ReceiverOptions)));
|
||||||
|
Services.Configure<TelegramBotClientOptions>(Configuration.GetSection(nameof(TelegramBotClientOptions)), new TelegramBotClientOptionsProxy());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the host.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public TelegramBotHost Build()
|
||||||
|
{
|
||||||
|
foreach (var preBuildRoutine in _handlers.PreBuilderRoutines)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
preBuildRoutine.Invoke(this);
|
||||||
|
}
|
||||||
|
catch (NotImplementedException)
|
||||||
|
{
|
||||||
|
_ = 0xBAD + 0xC0DE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TelegramBotHost(_innerBuilder, _handlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Telegrator.Configuration;
|
||||||
|
|
||||||
|
namespace Telegrator.Hosting
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class TelegramBotHostBuilderSettings() : IHandlersCollectingOptions
|
||||||
|
{
|
||||||
|
/// <inheritdoc cref="HostApplicationBuilderSettings.DisableDefaults"/>
|
||||||
|
public bool DisableDefaults { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="HostApplicationBuilderSettings.Args"/>
|
||||||
|
public string[]? Args { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="HostApplicationBuilderSettings.Configuration"/>
|
||||||
|
public ConfigurationManager? Configuration { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="HostApplicationBuilderSettings.EnvironmentName"/>
|
||||||
|
public string? EnvironmentName { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="HostApplicationBuilderSettings.ApplicationName"/>
|
||||||
|
public string? ApplicationName { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="HostApplicationBuilderSettings.ContentRootPath"/>
|
||||||
|
public string? ContentRootPath { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool DescendDescriptorIndex { get; set; } = true;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool ExceptIntersectingCommandAliases { get; set; } = true;
|
||||||
|
|
||||||
|
internal HostApplicationBuilderSettings ToApplicationBuilderSettings() => new HostApplicationBuilderSettings()
|
||||||
|
{
|
||||||
|
DisableDefaults = DisableDefaults,
|
||||||
|
Args = Args,
|
||||||
|
Configuration = Configuration,
|
||||||
|
EnvironmentName = EnvironmentName,
|
||||||
|
ApplicationName = ApplicationName,
|
||||||
|
ContentRootPath = ContentRootPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Configurations>Debug;Release</Configurations>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Telegrator\Telegrator.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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<TOptions>(this IServiceCollection services, IConfiguration configuration, ConfigureOptionsProxy<TOptions> optionsProxy) where TOptions : class
|
||||||
|
{
|
||||||
|
optionsProxy.Configure(services, configuration);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddTelegramBotHostDefaults(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddLogging(builder => builder.AddConsole());
|
||||||
|
services.AddSingleton<IUpdateHandlersPool, HostUpdateHandlersPool>();
|
||||||
|
services.AddSingleton<IAwaitingProvider, HostAwaitingProvider>();
|
||||||
|
services.AddSingleton<IHandlersProvider, HostHandlersProvider>();
|
||||||
|
services.AddSingleton<IUpdateRouter, HostUpdateRouter>();
|
||||||
|
services.AddSingleton<ITelegramBotInfo, TelegramBotInfo>(services => new TelegramBotInfo(services.GetRequiredService<ITelegramBotClient>().GetMe().Result));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddTelegramReceiver(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHttpClient<ITelegramBotClient>("tgreceiver").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory);
|
||||||
|
services.AddHostedService<HostedUpdateReceiver>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ITelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider)
|
||||||
|
=> new TelegramBotClient(provider.GetRequiredService<IOptions<TelegramBotClientOptions>>().Value, httpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TelegramBotHostExtensions
|
||||||
|
{
|
||||||
|
public static ITelegramBotHost SetBotCommands(this ITelegramBotHost botHost)
|
||||||
|
{
|
||||||
|
ITelegramBotClient client = botHost.Services.GetRequiredService<ITelegramBotClient>();
|
||||||
|
IEnumerable<BotCommand> aliases = botHost.UpdateRouter.HandlersProvider.GetBotCommands();
|
||||||
|
client.SetMyCommands(aliases).Wait();
|
||||||
|
return botHost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Тесты для коллекций.
|
||||||
|
///
|
||||||
|
/// ПАРАДИГМЫ ТЕСТИРОВАНИЯ:
|
||||||
|
/// 1. Collection Testing - тестирование коллекций и их операций
|
||||||
|
/// 2. List Testing - тестирование списков
|
||||||
|
/// 3. Indexing Testing - тестирование индексации
|
||||||
|
/// 4. Enumeration Testing - тестирование перечисления
|
||||||
|
/// 5. Capacity Testing - тестирование емкости коллекций
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - создание списка.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем создание коллекций
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void HandlerDescriptorList_ShouldBeCreated()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var list = new HandlerDescriptorList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
list.Should().NotBeNull();
|
||||||
|
list.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - добавление дескриптора.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем добавление элементов в коллекцию
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - добавление нескольких дескрипторов.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем множественные операции
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - получение по индексу.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем индексацию коллекций
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - получение по неверному индексу.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем исключения при некорректном доступе
|
||||||
|
/// </summary>
|
||||||
|
[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<ArgumentOutOfRangeException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - перечисление элементов.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем перечисление коллекций
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - очистка списка.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем очистку коллекций
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - проверка содержания элемента.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем поиск в коллекциях
|
||||||
|
/// </summary>
|
||||||
|
[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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - удаление элемента.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем удаление элементов из коллекций
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для HandlerDescriptorList - удаление несуществующего элемента.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем удаление несуществующих элементов
|
||||||
|
/// </summary>
|
||||||
|
[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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для CompletedFiltersList - создание списка.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем создание специализированных коллекций
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void CompletedFiltersList_ShouldBeCreated()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var list = new CompletedFiltersList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
list.Should().NotBeNull();
|
||||||
|
list.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для проверки производительности коллекций.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем производительность при большом количестве элементов
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для проверки потокобезопасности (базовый тест).
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем базовую потокобезопасность
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async void HandlerDescriptorList_ShouldHandleConcurrentAccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var list = new HandlerDescriptorList();
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вспомогательный метод для создания тестового дескриптора.
|
||||||
|
/// </summary>
|
||||||
|
private static HandlerDescriptor CreateTestDescriptor(UpdateType updateType)
|
||||||
|
{
|
||||||
|
return new HandlerDescriptor(DescriptorType.General, typeof(TestUpdateHandler));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Тесты для базовых фильтров.
|
||||||
|
///
|
||||||
|
/// ПАРАДИГМЫ ТЕСТИРОВАНИЯ:
|
||||||
|
/// 1. AAA (Arrange-Act-Assert) - структура теста: подготовка, действие, проверка
|
||||||
|
/// 2. Given-When-Then - альтернативная формулировка AAA для лучшей читаемости
|
||||||
|
/// 3. Тестирование граничных случаев и исключений
|
||||||
|
/// 4. Использование моков для изоляции тестируемого кода
|
||||||
|
/// 5. Тестирование как позитивных, так и негативных сценариев
|
||||||
|
/// </summary>
|
||||||
|
public class FilterTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для AnyFilter - фильтр, который всегда проходит.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем базовое поведение - фильтр должен всегда возвращать true
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void AnyFilter_ShouldAlwaysPass()
|
||||||
|
{
|
||||||
|
// Arrange (Given) - подготовка тестовых данных
|
||||||
|
var anyFilter = Filter<Update>.Any();
|
||||||
|
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
|
||||||
|
|
||||||
|
// Act (When) - выполнение тестируемого действия
|
||||||
|
var result = anyFilter.CanPass(context);
|
||||||
|
|
||||||
|
// Assert (Then) - проверка результата
|
||||||
|
result.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для ReverseFilter - инвертирование результата фильтра.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем композицию фильтров и логику инверсии
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ReverseFilter_ShouldInvertResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var alwaysTrueFilter = Filter<Update>.Any();
|
||||||
|
var reverseFilter = alwaysTrueFilter.Not();
|
||||||
|
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = reverseFilter.CanPass(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для AndFilter - логическое И между фильтрами.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем комбинирование фильтров и логику И
|
||||||
|
/// </summary>
|
||||||
|
[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<Update>.If(_ => firstResult);
|
||||||
|
var secondFilter = Filter<Update>.If(_ => secondResult);
|
||||||
|
var andFilter = firstFilter.And(secondFilter);
|
||||||
|
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = andFilter.CanPass(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(expectedResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для OrFilter - логическое ИЛИ между фильтрами.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем комбинирование фильтров и логику ИЛИ
|
||||||
|
/// </summary>
|
||||||
|
[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<Update>.If(_ => firstResult);
|
||||||
|
var secondFilter = Filter<Update>.If(_ => secondResult);
|
||||||
|
var orFilter = firstFilter.Or(secondFilter);
|
||||||
|
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = orFilter.CanPass(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(expectedResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для CompiledFilter - компиляция нескольких фильтров.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем сложную композицию фильтров
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void CompiledFilter_ShouldPassOnlyWhenAllFiltersPass()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filter1 = Filter<Update>.If(_ => true);
|
||||||
|
var filter2 = Filter<Update>.If(_ => true);
|
||||||
|
var filter3 = Filter<Update>.If(_ => false);
|
||||||
|
|
||||||
|
var compiledFilter = CompiledFilter<Update>.Compile(filter1, filter2, filter3);
|
||||||
|
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = compiledFilter.CanPass(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().BeFalse(); // Должен вернуть false, так как filter3 возвращает false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для проверки IsCollectible свойства.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем свойства объектов
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Filter_IsCollectible_ShouldBeTrueForAnyFilter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anyFilter = Filter<Update>.Any();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isCollectible = anyFilter.IsCollectible;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
isCollectible.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для FunctionFilter - фильтр на основе функции.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем создание фильтров из функций
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FunctionFilter_ShouldUseProvidedFunction()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var wasCalled = false;
|
||||||
|
var functionFilter = Filter<Update>.If(_ =>
|
||||||
|
{
|
||||||
|
wasCalled = true;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
var context = new FilterExecutionContext<Update>(new TelegramBotInfo(null), new Update(), new Update(), new Dictionary<string, object>(), new CompletedFiltersList());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = functionFilter.CanPass(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().BeTrue();
|
||||||
|
wasCalled.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Handlers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Telegrator.Tests.Handlers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Тесты для обработчиков обновлений.
|
||||||
|
///
|
||||||
|
/// ПАРАДИГМЫ ТЕСТИРОВАНИЯ:
|
||||||
|
/// 1. Mocking - создание моков для изоляции зависимостей
|
||||||
|
/// 2. Dependency Injection - тестирование через интерфейсы
|
||||||
|
/// 3. Test Doubles - использование заглушек вместо реальных объектов
|
||||||
|
/// 4. Behavior Verification - проверка поведения, а не только результата
|
||||||
|
/// 5. Exception Testing - тестирование исключений
|
||||||
|
/// </summary>
|
||||||
|
public class HandlerTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для базового обработчика обновлений.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем абстрактный класс через конкретную реализацию
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateHandlerBase_ShouldExecuteAndMarkLifetimeAsEnded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockContainer = new Mock<IAbstractHandlerContainer<Message>>();
|
||||||
|
var testHandler = new TestUpdateHandler();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await testHandler.Execute(mockContainer.Object);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
testHandler.WasExecuted.Should().BeTrue();
|
||||||
|
testHandler.LifetimeToken.IsEnded.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для проверки токена жизненного цикла.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем состояние объектов
|
||||||
|
/// </summary>
|
||||||
|
[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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тест для проверки отмены операции.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Тестируем асинхронные операции и отмену
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateHandlerBase_ShouldHandleCancellation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockContainer = new Mock<IAbstractHandlerContainer<Message>>();
|
||||||
|
var testHandler = new TestUpdateHandler();
|
||||||
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
cancellationTokenSource.Cancel(); // Отменяем сразу
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await testHandler.Invoking(h => h.Execute(mockContainer.Object, cancellationTokenSource.Token))
|
||||||
|
.Should().ThrowAsync<OperationCanceledException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Update>.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<ITelegramBotClient>();
|
||||||
|
var mockContainer = TestUtilities.CreateMockHandlerContainer();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
// Тестирование с моками
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Тестирование исключений
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void TestExceptions()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Action action = () => { /* код, который должен выбросить исключение */ };
|
||||||
|
action.Should().Throw<ArgumentException>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск тестов
|
||||||
|
|
||||||
|
### Через командную строку
|
||||||
|
```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/)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Configurations>Debug;Release</Configurations>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.6.6" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.70" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Telegrator\Telegrator.csproj" />
|
||||||
|
<ProjectReference Include="..\Telegrator.Hosting\Telegrator.Hosting.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Handlers;
|
||||||
|
|
||||||
|
namespace Telegrator.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Вспомогательный класс для тестирования абстрактного UpdateHandlerBase.
|
||||||
|
///
|
||||||
|
/// ПРИНЦИП: Создание тестовых двойников для абстрактных классов
|
||||||
|
/// </summary>
|
||||||
|
[MessageHandler]
|
||||||
|
internal class TestUpdateHandler : MessageHandler
|
||||||
|
{
|
||||||
|
public bool WasExecuted { get; private set; }
|
||||||
|
|
||||||
|
public override Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
WasExecuted = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Filters;
|
||||||
|
using Telegrator.Attributes;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on command aliases.
|
||||||
|
/// Allows handlers to respond to multiple command variations using a single attribute.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandAlliasAttribute : UpdateFilterAttribute<Message>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the allowed update types for this filter.
|
||||||
|
/// </summary>
|
||||||
|
public override UpdateType[] AllowedTypes => [UpdateType.Message];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The description of the command (defaults to "no description provided").
|
||||||
|
/// </summary>
|
||||||
|
private string _description = "no description provided";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the array of command aliases that this filter will match.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Alliases
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the description of the command.
|
||||||
|
/// Must be between 0 and 256 characters in length.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Thrown when the description length is outside the allowed range.</exception>
|
||||||
|
public string Description
|
||||||
|
{
|
||||||
|
get => _description;
|
||||||
|
set => _description = value is { Length: <= 256 and >= 0 }
|
||||||
|
? value : throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the CommandAlliasAttribute with the specified command aliases.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="alliases">The command aliases to match against.</param>
|
||||||
|
public CommandAlliasAttribute(params string[] alliases)
|
||||||
|
: base(new CommandAlliasFilter(alliases)) => Alliases = alliases;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the filtering target (Message) from the update.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="update">The Telegram update.</param>
|
||||||
|
/// <returns>The message from the update, or null if not present.</returns>
|
||||||
|
public override Message? GetFilterringTarget(Update update) => update.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base attribute for filtering updates based on environment conditions.
|
||||||
|
/// Can process all types of updates and provides environment-specific filtering logic.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filters">The environment filters to apply</param>
|
||||||
|
public abstract class EnvironmentFilterAttribute(params IFilter<Update>[] filters) : UpdateFilterAttribute<Update>(filters)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the allowed update types that this filter can process.
|
||||||
|
/// Environment filters can process all update types.
|
||||||
|
/// </summary>
|
||||||
|
public override UpdateType[] AllowedTypes => Update.AllTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the update as the filtering target.
|
||||||
|
/// Environment filters work with the entire update object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="update">The Telegram update</param>
|
||||||
|
/// <returns>The update object itself</returns>
|
||||||
|
public override Update? GetFilterringTarget(Update update)
|
||||||
|
=> update;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering updates that occur in debug environment.
|
||||||
|
/// Only allows updates when the application is running in debug mode.
|
||||||
|
/// </summary>
|
||||||
|
public class IsDebugEnvironmentAttribute()
|
||||||
|
: EnvironmentFilterAttribute(new IsDebugEnvironmentFilter())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering updates that occur in release environment.
|
||||||
|
/// Only allows updates when the application is running in release mode.
|
||||||
|
/// </summary>
|
||||||
|
public class IsReleaseEnvironmentAttribute()
|
||||||
|
: EnvironmentFilterAttribute(new IsReleaseEnvironmentFilter())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering updates based on environment variable values.
|
||||||
|
/// </summary>
|
||||||
|
public class EnvironmentVariableAttribute : EnvironmentFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter based on an environment variable with a specific value and comparison method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="variable">The name of the environment variable</param>
|
||||||
|
/// <param name="value">The expected value of the environment variable</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
public EnvironmentVariableAttribute(string variable, string? value, StringComparison comparison)
|
||||||
|
: base(new EnvironmentVariableFilter(variable, value, comparison)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter based on an environment variable with a specific value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="variable">The name of the environment variable</param>
|
||||||
|
/// <param name="value">The expected value of the environment variable</param>
|
||||||
|
public EnvironmentVariableAttribute(string variable, string? value)
|
||||||
|
: base(new EnvironmentVariableFilter(variable, value)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter based on the existence of an environment variable.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="variable">The name of the environment variable</param>
|
||||||
|
public EnvironmentVariableAttribute(string variable)
|
||||||
|
: base(new EnvironmentVariableFilter(variable)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter based on an environment variable with a specific comparison method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="variable">The name of the environment variable</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
public EnvironmentVariableAttribute(string variable, StringComparison comparison)
|
||||||
|
: base(new EnvironmentVariableFilter(variable, comparison)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages that contain mentions.
|
||||||
|
/// Allows handlers to respond only to messages that mention the bot or specific users.
|
||||||
|
/// </summary>
|
||||||
|
public class MentionedAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the MentionedAttribute that matches any mention.
|
||||||
|
/// </summary>
|
||||||
|
public MentionedAttribute()
|
||||||
|
: base(new MessageHasEntityFilter(MessageEntityType.Mention, 0, null), new MentionedFilter()) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the MentionedAttribute that matches mentions at a specific offset.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="offset">The offset position where the mention should occur.</param>
|
||||||
|
public MentionedAttribute(int offset)
|
||||||
|
: base(new MessageHasEntityFilter(MessageEntityType.Mention, offset, null), new MentionedFilter()) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the MentionedAttribute that matches a specific mention.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mention">The specific mention text to match.</param>
|
||||||
|
public MentionedAttribute(string mention)
|
||||||
|
: base(new MessageHasEntityFilter(MessageEntityType.Mention), new MentionedFilter(mention)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the MentionedAttribute that matches a specific mention at a specific offset.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mention">The specific mention text to match.</param>
|
||||||
|
/// <param name="offset">The offset position where the mention should occur.</param>
|
||||||
|
public MentionedAttribute(string mention, int offset)
|
||||||
|
: base(new MessageHasEntityFilter(MessageEntityType.Mention, offset, null), new MentionedFilter(mention)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages sent in forum chats.
|
||||||
|
/// </summary>
|
||||||
|
public class ChatIsForumAttribute()
|
||||||
|
: MessageFilterAttribute(new MessageChatIsForumFilter())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages sent in a specific chat by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The chat ID to match</param>
|
||||||
|
public class ChatIdAttribute(long id)
|
||||||
|
: MessageFilterAttribute(new MessageChatIdFilter(id))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages sent in chats of a specific type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The chat type to match</param>
|
||||||
|
public class ChatTypeAttribute(ChatType type)
|
||||||
|
: MessageFilterAttribute(new MessageChatTypeFilter(type))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the chat title.
|
||||||
|
/// </summary>
|
||||||
|
public class ChatTitleAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from chats with a specific title and comparison method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The chat title to match</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
public ChatTitleAttribute(string? title, StringComparison comparison)
|
||||||
|
: base(new MessageChatTitleFilter(title, comparison)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from chats with a specific title.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The chat title to match</param>
|
||||||
|
public ChatTitleAttribute(string? title)
|
||||||
|
: base(new MessageChatTitleFilter(title)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the chat username.
|
||||||
|
/// </summary>
|
||||||
|
public class ChatUsernameAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from chats with a specific username and comparison method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userName">The chat username to match</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
public ChatUsernameAttribute(string? userName, StringComparison comparison)
|
||||||
|
: base(new MessageChatUsernameFilter(userName, comparison)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from chats with a specific username.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userName">The chat username to match</param>
|
||||||
|
public ChatUsernameAttribute(string? userName)
|
||||||
|
: base(new MessageChatUsernameFilter(userName, StringComparison.InvariantCulture)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the chat name (first name and optionally last name).
|
||||||
|
/// </summary>
|
||||||
|
public class ChatNameAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from chats with specific first and last names.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="lastName">The last name to match (optional)</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
public ChatNameAttribute(string? firstName, string? lastName, StringComparison comparison)
|
||||||
|
: base(new MessageChatNameFilter(firstName, lastName, comparison)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from chats with specific first and last names.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="lastName">The last name to match (optional)</param>
|
||||||
|
public ChatNameAttribute(string? firstName, string? lastName)
|
||||||
|
: base(new MessageChatNameFilter(firstName, lastName)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base attribute for filtering message-based updates.
|
||||||
|
/// Supports various message types including regular messages, edited messages, channel posts, and business messages.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filters">The filters to apply to messages</param>
|
||||||
|
public abstract class MessageFilterAttribute(params IFilter<Message>[] filters) : UpdateFilterAttribute<Message>(filters)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the allowed update types that this filter can process.
|
||||||
|
/// </summary>
|
||||||
|
public override UpdateType[] AllowedTypes =>
|
||||||
|
[
|
||||||
|
UpdateType.Message,
|
||||||
|
UpdateType.EditedMessage,
|
||||||
|
UpdateType.ChannelPost,
|
||||||
|
UpdateType.EditedChannelPost,
|
||||||
|
UpdateType.BusinessMessage,
|
||||||
|
UpdateType.EditedBusinessMessage
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the message from various types of updates.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="update">The Telegram update</param>
|
||||||
|
/// <returns>The message from the update, or null if not present</returns>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on regular expression patterns.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageRegexAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a regex pattern and options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pattern">The regular expression pattern to match</param>
|
||||||
|
/// <param name="regexOptions">The regex options for matching</param>
|
||||||
|
public MessageRegexAttribute(string pattern, RegexOptions regexOptions = default)
|
||||||
|
: base(new MessageRegexFilter(pattern, regexOptions)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a precompiled regex.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="regex">The precompiled regular expression</param>
|
||||||
|
public MessageRegexAttribute(Regex regex)
|
||||||
|
: base(new MessageRegexFilter(regex)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages that contain dice throws with specific values.
|
||||||
|
/// </summary>
|
||||||
|
public class DiceThrowedAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter dice throws with a specific value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The dice value to match</param>
|
||||||
|
public DiceThrowedAttribute(int value)
|
||||||
|
: base(new DiceThrowedFilter(value)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter dice throws with a specific type and value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="diceType">The type of dice</param>
|
||||||
|
/// <param name="value">The dice value to match</param>
|
||||||
|
public DiceThrowedAttribute(DiceType diceType, int value)
|
||||||
|
: base(new DiceThrowedFilter(diceType, value)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages that are automatically forwarded.
|
||||||
|
/// </summary>
|
||||||
|
public class IsAutomaticFormwardMessageAttribute()
|
||||||
|
: MessageFilterAttribute(new IsAutomaticFormwardMessageFilter())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages sent while the user was offline.
|
||||||
|
/// </summary>
|
||||||
|
public class IsFromOfflineMessageAttribute()
|
||||||
|
: MessageFilterAttribute(new IsFromOfflineMessageFilter())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering service messages (e.g., user joined, left, etc.).
|
||||||
|
/// </summary>
|
||||||
|
public class IsServiceMessageMessageAttribute()
|
||||||
|
: MessageFilterAttribute(new IsServiceMessageMessageFilter())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering topic messages in forum chats.
|
||||||
|
/// </summary>
|
||||||
|
public class IsTopicMessageMessageAttribute()
|
||||||
|
: MessageFilterAttribute(new IsServiceMessageMessageFilter())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on their entities (mentions, links, etc.).
|
||||||
|
/// </summary>
|
||||||
|
public class MessageHasEntityAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages with a specific entity type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to match</param>
|
||||||
|
public MessageHasEntityAttribute(MessageEntityType type)
|
||||||
|
: base(new MessageHasEntityFilter(type)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages with a specific entity type at a specific position.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to match</param>
|
||||||
|
/// <param name="offset">The starting position of the entity</param>
|
||||||
|
/// <param name="length">The length of the entity (optional)</param>
|
||||||
|
public MessageHasEntityAttribute(MessageEntityType type, int offset, int? length)
|
||||||
|
: base(new MessageHasEntityFilter(type, offset, length)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages with a specific entity type and content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to match</param>
|
||||||
|
/// <param name="content">The content that the entity should contain</param>
|
||||||
|
/// <param name="stringComparison">The string comparison method</param>
|
||||||
|
public MessageHasEntityAttribute(MessageEntityType type, string content, StringComparison stringComparison = StringComparison.CurrentCulture)
|
||||||
|
: base(new MessageHasEntityFilter(type, content, stringComparison)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages with a specific entity type, position, and content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to match</param>
|
||||||
|
/// <param name="offset">The starting position of the entity</param>
|
||||||
|
/// <param name="length">The length of the entity (optional)</param>
|
||||||
|
/// <param name="content">The content that the entity should contain</param>
|
||||||
|
/// <param name="stringComparison">The string comparison method</param>
|
||||||
|
public MessageHasEntityAttribute(MessageEntityType type, int offset, int? length, string content, StringComparison stringComparison = StringComparison.CurrentCulture)
|
||||||
|
: base(new MessageHasEntityFilter(type, offset, length, content, stringComparison)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the sender's username.
|
||||||
|
/// </summary>
|
||||||
|
public class FromUsernameAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from a specific username.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username to match</param>
|
||||||
|
public FromUsernameAttribute(string username)
|
||||||
|
: base(new FromUsernameFilter(username)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from a specific username with custom comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username to match</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
public FromUsernameAttribute(string username, StringComparison comparison)
|
||||||
|
: base(new FromUsernameFilter(username, comparison)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the sender's name (first name and optionally last name).
|
||||||
|
/// </summary>
|
||||||
|
public class FromUserAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from a user with specific first and last names.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="lastName">The last name to match (optional)</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
public FromUserAttribute(string firstName, string? lastName, StringComparison comparison)
|
||||||
|
: base(new FromUserFilter(firstName, lastName, comparison)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from a user with specific first and last names.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="lastName">The last name to match</param>
|
||||||
|
public FromUserAttribute(string firstName, string? lastName)
|
||||||
|
: base(new FromUserFilter(firstName, lastName, StringComparison.InvariantCulture)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from a user with a specific first name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
public FromUserAttribute(string firstName)
|
||||||
|
: base(new FromUserFilter(firstName, null, StringComparison.InvariantCulture)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages from a user with a specific first name and custom comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
public FromUserAttribute(string firstName, StringComparison comparison)
|
||||||
|
: base(new FromUserFilter(firstName, null, comparison)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages from a specific user ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user ID to match</param>
|
||||||
|
public class FromUserIdAttribute(long userId)
|
||||||
|
: MessageFilterAttribute(new FromUserIdFilter(userId))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages sent by not bots (users).
|
||||||
|
/// </summary>
|
||||||
|
public class NotFromBotAttribute()
|
||||||
|
: MessageFilterAttribute(new FromBotFilter().Not())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages sent by bots.
|
||||||
|
/// </summary>
|
||||||
|
public class FromBotAttribute()
|
||||||
|
: MessageFilterAttribute(new FromBotFilter())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages sent by premium users.
|
||||||
|
/// </summary>
|
||||||
|
public class FromPremiumUserAttribute()
|
||||||
|
: MessageFilterAttribute(new FromPremiumUserFilter())
|
||||||
|
{ }
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the text starts with the specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The string that the message text should start with</param>
|
||||||
|
/// <param name="comparison">The string comparison type</param>
|
||||||
|
public class TextStartsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture)
|
||||||
|
: MessageFilterAttribute(new TextStartsWithFilter(content, comparison))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the text ends with the specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The string that the message text should end with</param>
|
||||||
|
/// <param name="comparison">The string comparison type</param>
|
||||||
|
public class TextEndsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture)
|
||||||
|
: MessageFilterAttribute(new TextEndsWithFilter(content, comparison))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the text contains the specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The string that the message text should contain</param>
|
||||||
|
/// <param name="comparison">The string comparison type</param>
|
||||||
|
public class TextContainsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture)
|
||||||
|
: MessageFilterAttribute(new TextContainsFilter(content, comparison))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the text equals the specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The string that the message text should equal</param>
|
||||||
|
/// <param name="comparison">The string comparison type</param>
|
||||||
|
public class TextEqualsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture)
|
||||||
|
: MessageFilterAttribute(new TextEqualsFilter(content, comparison))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages that contain any non-empty text.
|
||||||
|
/// </summary>
|
||||||
|
public class HasTextAttribute()
|
||||||
|
: MessageFilterAttribute(new TextNotNullOrEmptyFilter())
|
||||||
|
{ }
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages that are replies to messages containing mentions.
|
||||||
|
/// Allows handlers to respond to messages that reply to messages with specific mentions.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedMentionedAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the RepliedMentionedAttribute that matches replies to any mention.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">The depth of the reply chain to check (default: 1).</param>
|
||||||
|
public RepliedMentionedAttribute(int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageHasEntityFilter(MessageEntityType.Mention, 0, null, replyDepth), new RepliedMentionedFilter(replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the RepliedMentionedAttribute that matches replies to mentions at a specific offset.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="offset">The offset position where the mention should occur in the replied message.</param>
|
||||||
|
/// <param name="replyDepth">The depth of the reply chain to check (default: 1).</param>
|
||||||
|
public RepliedMentionedAttribute(int offset, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageHasEntityFilter(MessageEntityType.Mention, offset, null, replyDepth), new RepliedMentionedFilter(replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the RepliedMentionedAttribute that matches replies to a specific mention.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mention">The specific mention text to match in the replied message.</param>
|
||||||
|
/// <param name="replyDepth">The depth of the reply chain to check (default: 1).</param>
|
||||||
|
public RepliedMentionedAttribute(string mention, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageHasEntityFilter(MessageEntityType.Mention, replyDepth), new RepliedMentionedFilter(mention, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the RepliedMentionedAttribute that matches replies to a specific mention at a specific offset.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mention">The specific mention text to match in the replied message.</param>
|
||||||
|
/// <param name="offset">The offset position where the mention should occur in the replied message.</param>
|
||||||
|
/// <param name="replyDepth">The depth of the reply chain to check (default: 1).</param>
|
||||||
|
public RepliedMentionedAttribute(string mention, int offset, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageHasEntityFilter(MessageEntityType.Mention, offset, null, replyDepth), new RepliedMentionedFilter(mention, replyDepth)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message was sent in a forum chat.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedChatIsForumAttribute(int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedMessageChatIsForumFilter(replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message was sent in a specific chat by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The chat ID to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedChatIdAttribute(long id, int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedMessageChatIdFilter(id, replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message was sent in a chat of a specific type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The chat type to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedChatTypeAttribute(ChatType type, int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedMessageChatTypeFilter(type, replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the chat title of the replied-to message.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedChatTitleAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a chat with a specific title and comparison method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The chat title to match</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedChatTitleAttribute(string? title, StringComparison comparison, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageChatTitleFilter(title, comparison, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a chat with a specific title.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The chat title to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedChatTitleAttribute(string? title, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageChatTitleFilter(title, StringComparison.InvariantCulture, replyDepth)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the chat username of the replied-to message.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedChatUsernameAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a chat with a specific username and comparison method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userName">The chat username to match</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedChatUsernameAttribute(string? userName, StringComparison comparison, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageChatUsernameFilter(userName, comparison, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a chat with a specific username.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userName">The chat username to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedChatUsernameAttribute(string? userName, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageChatUsernameFilter(userName, replyDepth)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the chat name of the replied-to message.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedChatNameAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a chat with specific first and last names.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="lastName">The last name to match (optional)</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedChatNameAttribute(string? firstName, string? lastName, StringComparison comparison, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageChatNameFilter(firstName, lastName, comparison, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a chat with specific first and last names.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="lastName">The last name to match (optional)</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedChatNameAttribute(string? firstName, string? lastName, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageChatNameFilter(firstName, lastName, StringComparison.InvariantCulture, replyDepth)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages that are replies to other messages.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class MessageRepliedAttribute(int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new MessageRepliedFilter(replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message contains dice throws with specific values.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedDiceThrowedAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message contains a dice throw with a specific value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The dice value to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedDiceThrowedAttribute(int value, int replyDepth = 1)
|
||||||
|
: base(new RepliedDiceThrowedFilter(value, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message contains a dice throw with a specific type and value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="diceType">The type of dice</param>
|
||||||
|
/// <param name="value">The dice value to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedDiceThrowedAttribute(DiceType diceType, int value, int replyDepth = 1)
|
||||||
|
: base(new RepliedDiceThrowedFilter(diceType, value, replyDepth)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message was automatically forwarded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedIsAutomaticFormwardMessageAttribute(int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedIsAutomaticFormwardMessageFilter(replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message was sent while the user was offline.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedIsFromOfflineMessageAttribute(int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedIsFromOfflineMessageFilter(replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message is a service message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedIsServiceMessageMessageAttribute(int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedIsServiceMessageMessageFilter(replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message is a topic message in forum chats.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedIsTopicMessageMessageAttribut(int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedIsServiceMessageMessageFilter(replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on entities in the replied-to message.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedMessageHasEntityAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message has a specific entity type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedMessageHasEntityAttribute(MessageEntityType type, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageHasEntityFilter(type, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message has a specific entity type at a specific position.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to match</param>
|
||||||
|
/// <param name="offset">The starting position of the entity</param>
|
||||||
|
/// <param name="length">The length of the entity (optional)</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedMessageHasEntityAttribute(MessageEntityType type, int offset, int? length, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageHasEntityFilter(type, offset, length, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message has a specific entity type with specific content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to match</param>
|
||||||
|
/// <param name="content">The content that the entity should contain</param>
|
||||||
|
/// <param name="stringComparison">The string comparison method</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedMessageHasEntityAttribute(MessageEntityType type, string content, StringComparison stringComparison = StringComparison.CurrentCulture, int replyDepth = 1)
|
||||||
|
: base(new RepliedMessageHasEntityFilter(type, content, stringComparison, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message has a specific entity type at a specific position with specific content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to match</param>
|
||||||
|
/// <param name="offset">The starting position of the entity</param>
|
||||||
|
/// <param name="length">The length of the entity (optional)</param>
|
||||||
|
/// <param name="content">The content that the entity should contain</param>
|
||||||
|
/// <param name="stringComparison">The string comparison method</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
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)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the username of the sender of the replied-to message.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedFromUsernameAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a specific username.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedFromUsernameAttribute(string username, int replyDepth = 1)
|
||||||
|
: base(new RepliedUsernameFilter(username, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a specific username with custom comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username to match</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedFromUsernameAttribute(string username, StringComparison comparison, int replyDepth = 1)
|
||||||
|
: base(new RepliedUsernameFilter(username, comparison, replyDepth)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the name of the sender of the replied-to message.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedFromUserAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a user with specific names.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="lastName">The last name to match (optional)</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedFromUserAttribute(string firstName, string? lastName, StringComparison comparison, int replyDepth = 1)
|
||||||
|
: base(new RepliedUserFilter(firstName, lastName, comparison, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a user with specific names.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="lastName">The last name to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedFromUserAttribute(string firstName, string lastName, int replyDepth = 1)
|
||||||
|
: base(new RepliedUserFilter(firstName, lastName, StringComparison.InvariantCulture, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a user with a specific first name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedFromUserAttribute(string firstName, int replyDepth = 1)
|
||||||
|
: base(new RepliedUserFilter(firstName, null, StringComparison.InvariantCulture, replyDepth)) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute to filter messages where the replied-to message is from a user with a specific first name and custom comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to match</param>
|
||||||
|
/// <param name="comparison">The string comparison method</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public RepliedFromUserAttribute(string firstName, StringComparison comparison, int replyDepth = 1)
|
||||||
|
: base(new RepliedUserFilter(firstName, null, comparison, replyDepth)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages based on the user ID of the sender of the replied-to message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user ID to match</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedUserIdAttribute(long userId, int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedUserIdFilter(userId, replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message was sent by a bot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class ReplyFromBotAttribute(int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new ReplyFromBotFilter(replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages where the replied-to message was sent by a premium user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class ReplyFromPremiumUserAttribute(int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new ReplyFromPremiumUserFilter(replyDepth))
|
||||||
|
{ }
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering updates where the replied-to message's text starts with the specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The string that the replied message's text should start with</param>
|
||||||
|
/// <param name="comparison">The string comparison type</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedTextStartsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedTextStartsWithFilter(content, comparison, replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering updates where the replied-to message's text ends with the specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The string that the replied message's text should end with</param>
|
||||||
|
/// <param name="comparison">The string comparison type</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedTextEndsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedTextEndsWithFilter(content, comparison, replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering updates where the replied-to message's text contains the specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The string that the replied message's text should contain</param>
|
||||||
|
/// <param name="comparison">The string comparison type</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedTextContainsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedTextContainsFilter(content, comparison, replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering updates where the replied-to message's text equals the specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The string that the replied message's text should equal</param>
|
||||||
|
/// <param name="comparison">The string comparison type</param>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedTextEqualsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedTextEqualsFilter(content, comparison, replyDepth))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering updates where the replied-to message contains any non-empty text.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">How many levels up the reply chain to check (default: 1)</param>
|
||||||
|
public class RepliedHasTextAttribute(int replyDepth = 1)
|
||||||
|
: MessageFilterAttribute(new RepliedTextNotNullOrEmptyFilter(replyDepth))
|
||||||
|
{ }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering messages with reply to messages of this bot.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedToMeAttribute()
|
||||||
|
: MessageFilterAttribute(new RepliedToMeFilter())
|
||||||
|
{ }
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using Telegrator.StateKeeping;
|
||||||
|
using Telegrator.Attributes;
|
||||||
|
using Telegrator.StateKeeping.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations.StateKeeping
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for managing enum-based states in Telegram bot handlers.
|
||||||
|
/// Provides a convenient way to associate enum values with state management functionality.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnum">The enum type to be used for state management.</typeparam>
|
||||||
|
public class EnumStateAttribute<TEnum> : StateKeeperAttribute<long, TEnum, EnumStateKeeper<TEnum>> where TEnum : Enum
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the EnumStateAttribute with a special state and custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specialState">The special state to be managed.</param>
|
||||||
|
/// <param name="keyResolver">The resolver for extracting keys from updates.</param>
|
||||||
|
public EnumStateAttribute(SpecialState specialState, IStateKeyResolver<long> keyResolver)
|
||||||
|
: base(specialState, keyResolver) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the EnumStateAttribute with a specific enum state and custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="myState">The specific enum state to be managed.</param>
|
||||||
|
/// <param name="keyResolver">The resolver for extracting keys from updates.</param>
|
||||||
|
public EnumStateAttribute(TEnum myState, IStateKeyResolver<long> keyResolver)
|
||||||
|
: base(myState, keyResolver) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the EnumStateAttribute with a special state and default sender ID resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specialState">The special state to be managed.</param>
|
||||||
|
public EnumStateAttribute(SpecialState specialState)
|
||||||
|
: base(specialState, new SenderIdResolver()) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the EnumStateAttribute with a specific enum state and default sender ID resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="myState">The specific enum state to be managed.</param>
|
||||||
|
public EnumStateAttribute(TEnum myState)
|
||||||
|
: this(myState, new SenderIdResolver()) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Telegrator.StateKeeping;
|
||||||
|
using Telegrator.Attributes;
|
||||||
|
using Telegrator.StateKeeping.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations.StateKeeping
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for associating a handler or method with a numeric (integer) state keeper.
|
||||||
|
/// Provides constructors for flexible state and key resolver configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class NumericStateAttribute : StateKeeperAttribute<long, int, NumericStateKeeper>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a special state and a custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specialState">The special state to associate</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
public NumericStateAttribute(SpecialState specialState, IStateKeyResolver<long> keyResolver)
|
||||||
|
: base(specialState, keyResolver) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a specific numeric state and a custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="myState">The integer state to associate</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
public NumericStateAttribute(int myState, IStateKeyResolver<long> keyResolver)
|
||||||
|
: base(myState, keyResolver) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a special state and the default sender ID resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specialState">The special state to associate</param>
|
||||||
|
public NumericStateAttribute(SpecialState specialState)
|
||||||
|
: base(specialState, new SenderIdResolver()) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a specific numeric state and the default sender ID resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="myState">The integer state to associate</param>
|
||||||
|
public NumericStateAttribute(int myState)
|
||||||
|
: this(myState, new SenderIdResolver()) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Telegrator.Annotations.StateKeeping
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents special states for state keeping logic.
|
||||||
|
/// </summary>
|
||||||
|
public enum SpecialState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No special state.
|
||||||
|
/// </summary>
|
||||||
|
None,
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that no state is present.
|
||||||
|
/// </summary>
|
||||||
|
NoState,
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that any state is acceptable.
|
||||||
|
/// </summary>
|
||||||
|
AnyState
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Telegrator.StateKeeping;
|
||||||
|
using Telegrator.Attributes;
|
||||||
|
using Telegrator.StateKeeping.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations.StateKeeping
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for associating a handler or method with a string-based state keeper.
|
||||||
|
/// Provides various constructors for flexible state and key resolver configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class StringStateAttribute : StateKeeperAttribute<long, string, StringStateKeeper>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a special state and a custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specialState">The special state to associate</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
public StringStateAttribute(SpecialState specialState, IStateKeyResolver<long> keyResolver)
|
||||||
|
: base(specialState, keyResolver) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a specific state and a custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="myState">The string state to associate</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
public StringStateAttribute(string myState, IStateKeyResolver<long> keyResolver)
|
||||||
|
: base(myState, keyResolver) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a special state and the default sender ID resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specialState">The special state to associate</param>
|
||||||
|
public StringStateAttribute(SpecialState specialState)
|
||||||
|
: base(specialState, new SenderIdResolver()) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a specific state and the default sender ID resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="myState">The string state to associate</param>
|
||||||
|
public StringStateAttribute(string myState)
|
||||||
|
: base(myState, new SenderIdResolver()) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a specific state, a custom key resolver, and a set of possible states.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="myState">The string state to associate</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
/// <param name="states">The set of possible string states</param>
|
||||||
|
public StringStateAttribute(string myState, IStateKeyResolver<long> keyResolver, params string[] states)
|
||||||
|
: base(new StringStateKeeper(states), myState, keyResolver) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a special state, a custom key resolver, and a set of possible states.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specialState">The special state to associate</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
/// <param name="states">The set of possible string states</param>
|
||||||
|
public StringStateAttribute(SpecialState specialState, IStateKeyResolver<long> keyResolver, params string[] states)
|
||||||
|
: base(new StringStateKeeper(states), specialState, keyResolver) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a specific state, the default sender ID resolver, and a set of possible states.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="myState">The string state to associate</param>
|
||||||
|
/// <param name="states">The set of possible string states</param>
|
||||||
|
public StringStateAttribute(string myState, params string[] states)
|
||||||
|
: base(new StringStateKeeper(states), myState, new SenderIdResolver()) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a special state, the default sender ID resolver, and a set of possible states.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specialState">The special state to associate</param>
|
||||||
|
/// <param name="states">The set of possible string states</param>
|
||||||
|
public StringStateAttribute(SpecialState specialState, params string[] states)
|
||||||
|
: base(new StringStateKeeper(states), specialState, new SenderIdResolver()) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Filters;
|
||||||
|
|
||||||
|
namespace Telegrator.Annotations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for filtering message with command "start" in bot's private chats.
|
||||||
|
/// Allows handlers to respond to "welcome" bot commands.
|
||||||
|
/// </summary>
|
||||||
|
public class WelcomeAttribute : MessageFilterAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates new instance of <see cref="WelcomeAttribute"/>
|
||||||
|
/// </summary>
|
||||||
|
public WelcomeAttribute() : base(new MessageChatTypeFilter(ChatType.Private), new CommandAlliasFilter("start"))
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
using Telegrator.Handlers.Components;
|
||||||
|
using Telegrator.StateKeeping.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Attributes.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the state in which the <see cref="UpdateHandlerBase"/> can be executed
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
|
public abstract class StateKeeperAttributeBase : Attribute, IFilter<Update>
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsCollectible => this.HasPublicProperties();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance <see cref="StateKeeperBase{TKey, TState}"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stateKeeperType"></param>
|
||||||
|
/// <exception cref="ArgumentException"></exception>
|
||||||
|
protected StateKeeperAttributeBase(Type stateKeeperType)
|
||||||
|
{
|
||||||
|
if (!stateKeeperType.IsAssignableToGenericType(typeof(StateKeeperBase<,>)))
|
||||||
|
throw new ArgumentException(stateKeeperType + " is not a StateKeeperBase", nameof(stateKeeperType));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Realizes a <see cref="IFilter{T}"/> for validation of the current <see cref="StateKeeperBase{TKey, TState}"/> in the polling routing
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public abstract bool CanPass(FilterExecutionContext<Update> context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the <see cref="IFilter{T}"/> to <see cref="Update"/> validation for entry into execution of the <see cref="UpdateHandlerBase"/>
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public abstract class UpdateFilterAttributeBase : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="UpdateType"/>'s that <see cref="UpdateHandlerBase"/> processing
|
||||||
|
/// </summary>
|
||||||
|
public abstract UpdateType[] AllowedTypes { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IFilter{T}"/> that <see cref="UpdateHandlerBase"/> processing
|
||||||
|
/// </summary>
|
||||||
|
public abstract Filter<Update> AnonymousFilter { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the filter modifiers that affect how this filter is combined with others.
|
||||||
|
/// </summary>
|
||||||
|
public FilterModifier Modifiers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="UpdateHandlerAttributeBase"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentException"></exception>
|
||||||
|
protected internal UpdateFilterAttributeBase()
|
||||||
|
{
|
||||||
|
if (AllowedTypes.Length == 0)
|
||||||
|
throw new ArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the logic of filter modifiers. Exceptionally internal implementation</summary>
|
||||||
|
/// <param name="previous"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public abstract bool ProcessModifiers(UpdateFilterAttributeBase? previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the <see cref="UpdateType"/>'s and validator (<see cref="IFilter{T}"/>) of the <see cref="Update"/> that <see cref="UpdateHandlerBase"/> will process
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
|
public abstract class UpdateHandlerAttributeBase : Attribute, IFilter<Update>
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsCollectible => this.HasPublicProperties();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an array of <see cref="UpdateHandlerBase"/> that this attribute can be attached to
|
||||||
|
/// </summary>
|
||||||
|
public Type[] ExpectingHandlerType { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an <see cref="UpdateType"/> that handlers processes
|
||||||
|
/// </summary>
|
||||||
|
public UpdateType Type { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets concurrency of this <see cref="UpdateHandlerBase"/> in same <see cref="UpdateType"/> pool
|
||||||
|
/// </summary>
|
||||||
|
public int Concurrency { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets priority of this <see cref="UpdateHandlerBase"/> in same type handlers pool
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="UpdateHandlerAttributeBase"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="expectingHandlerType"></param>
|
||||||
|
/// <param name="updateType"></param>
|
||||||
|
/// <param name="concurrency"></param>
|
||||||
|
/// <exception cref="ArgumentNullException"></exception>
|
||||||
|
/// <exception cref="ArgumentException"></exception>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an <see cref="DescriptorIndexer"/> of this <see cref="UpdateHandlerAttributeBase"/> from <see cref="Concurrency"/> and <see cref="Priority"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DescriptorIndexer GetIndexer()
|
||||||
|
=> new DescriptorIndexer(0, this);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validator (<see cref="IFilter{T}"/>) of the <see cref="Update"/> that <see cref="UpdateHandlerBase"/> will process
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public abstract bool CanPass(FilterExecutionContext<Update> context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Telegrator.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class DontCollectAttribute : Attribute
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace Telegrator.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enumeration of filter modifiers that can be applied to update filters.
|
||||||
|
/// Defines how filters should be combined and applied in filter chains.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum FilterModifier
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No modifier applied. Filter is applied as-is.
|
||||||
|
/// </summary>
|
||||||
|
None = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OR modifier. This filter or the next filter in the chain should match.
|
||||||
|
/// </summary>
|
||||||
|
OrNext = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// NOT modifier. The inverse of this filter should match.
|
||||||
|
/// </summary>
|
||||||
|
Not = 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
|
namespace Telegrator.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute that says if this handler cn await some of await types, that is not listed by its handler base
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class MightAwaitAttribute : Attribute
|
||||||
|
{
|
||||||
|
private readonly UpdateType[] _updateTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update types that may be awaited
|
||||||
|
/// </summary>
|
||||||
|
public UpdateType[] UpdateTypes => _updateTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// main ctor of <see cref="MightAwaitAttribute"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="updateTypes"></param>
|
||||||
|
public MightAwaitAttribute(params UpdateType[] updateTypes)
|
||||||
|
=> _updateTypes = updateTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract attribute for associating a handler or method with a state keeper.
|
||||||
|
/// Provides logic for state-based filtering and state management.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">The type of the key used for state keeping (e.g., chat ID).</typeparam>
|
||||||
|
/// <typeparam name="TState">The type of the state value (e.g., string, int).</typeparam>
|
||||||
|
/// <typeparam name="TKeeper">The type of the state keeper implementation.</typeparam>
|
||||||
|
public abstract class StateKeeperAttribute<TKey, TState, TKeeper> : StateKeeperAttributeBase where TKey : notnull where TState : notnull where TKeeper : StateKeeperBase<TKey, TState>, new()
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the singleton instance of the state keeper for this attribute type.
|
||||||
|
/// </summary>
|
||||||
|
public static TKeeper StateKeeper { get; internal set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the state value associated with this attribute instance.
|
||||||
|
/// </summary>
|
||||||
|
public TState MyState { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the special state mode for this attribute instance.
|
||||||
|
/// </summary>
|
||||||
|
public SpecialState SpecialState { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a specific state and a custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="myState">The state value to associate</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
protected StateKeeperAttribute(TState myState, IStateKeyResolver<TKey> keyResolver) : base(typeof(TKeeper))
|
||||||
|
{
|
||||||
|
StateKeeper ??= new TKeeper();
|
||||||
|
StateKeeper.KeyResolver = keyResolver;
|
||||||
|
MyState = myState;
|
||||||
|
SpecialState = SpecialState.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a special state and a custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specialState">The special state mode</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
protected StateKeeperAttribute(SpecialState specialState, IStateKeyResolver<TKey> keyResolver) : base(typeof(TKeeper))
|
||||||
|
{
|
||||||
|
StateKeeper ??= new TKeeper();
|
||||||
|
StateKeeper.KeyResolver = keyResolver;
|
||||||
|
MyState = StateKeeper.DefaultState;
|
||||||
|
SpecialState = specialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a custom state keeper, a specific state, and a custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keeper">The state keeper instance</param>
|
||||||
|
/// <param name="myState">The state value to associate</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
protected StateKeeperAttribute(TKeeper keeper, TState myState, IStateKeyResolver<TKey> keyResolver) : base(typeof(TKeeper))
|
||||||
|
{
|
||||||
|
StateKeeper ??= keeper;
|
||||||
|
StateKeeper.KeyResolver = keyResolver;
|
||||||
|
MyState = myState;
|
||||||
|
SpecialState = SpecialState.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a custom state keeper, a special state, and a custom key resolver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keeper">The state keeper instance</param>
|
||||||
|
/// <param name="specialState">The special state mode</param>
|
||||||
|
/// <param name="keyResolver">The key resolver for state keeping</param>
|
||||||
|
protected StateKeeperAttribute(TKeeper keeper, SpecialState specialState, IStateKeyResolver<TKey> keyResolver) : base(typeof(TKeeper))
|
||||||
|
{
|
||||||
|
StateKeeper ??= keeper;
|
||||||
|
StateKeeper.KeyResolver = keyResolver;
|
||||||
|
MyState = StateKeeper.DefaultState;
|
||||||
|
SpecialState = specialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the current update context passes the state filter.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context</param>
|
||||||
|
/// <returns>True if the state matches the filter; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Update> context)
|
||||||
|
{
|
||||||
|
if (SpecialState == SpecialState.AnyState)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!StateKeeper.TryGetState(context.Input, out TState? state))
|
||||||
|
return SpecialState == SpecialState.NoState;
|
||||||
|
|
||||||
|
if (state == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return MyState.Equals(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Attributes.Components;
|
||||||
|
using Telegrator.Filters;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base attribute for defining update filters for a specific type of update target.
|
||||||
|
/// Provides logic for filter composition, modifier processing, and target extraction.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the update target to filter (e.g., Message, Update).</typeparam>
|
||||||
|
public abstract class UpdateFilterAttribute<T> : UpdateFilterAttributeBase where T : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the compiled anonymous filter for this attribute.
|
||||||
|
/// </summary>
|
||||||
|
public override Filter<Update> AnonymousFilter { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the compiled filter logic for the update target.
|
||||||
|
/// </summary>
|
||||||
|
public Filter<T> UpdateFilter { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with one or more filters for the update target.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filters">The filters to compose</param>
|
||||||
|
protected UpdateFilterAttribute(params IFilter<T>[] filters)
|
||||||
|
{
|
||||||
|
UpdateFilter = CompiledFilter<T>.Compile(filters);
|
||||||
|
AnonymousFilter = AnonymousTypeFilter.Compile(UpdateFilter, GetFilterringTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the attribute with a precompiled filter for the update target.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="updateFilter">The compiled filter</param>
|
||||||
|
protected UpdateFilterAttribute(Filter<T> updateFilter)
|
||||||
|
{
|
||||||
|
UpdateFilter = updateFilter;
|
||||||
|
AnonymousFilter = AnonymousTypeFilter.Compile(UpdateFilter, GetFilterringTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes filter modifiers and combines this filter with the previous one if needed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="previous">The previous filter attribute in the chain</param>
|
||||||
|
/// <returns>True if the OrNext modifier is set; otherwise, false.</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the filtering target of type <typeparamref name="T"/> from the given update.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="update">The Telegram update</param>
|
||||||
|
/// <returns>The target object to filter, or null if not applicable</returns>
|
||||||
|
public abstract T? GetFilterringTarget(Update update);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Attributes.Components;
|
||||||
|
using Telegrator.Handlers.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base attribute for marking update handler classes.
|
||||||
|
/// Provides a type-safe way to associate handler types with specific update types and concurrency settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the update handler that this attribute is applied to.</typeparam>
|
||||||
|
/// <param name="updateType">The type of update that this handler can process.</param>
|
||||||
|
/// <param name="concurrency">The concurrency level for this handler (default: 0 for unlimited).</param>
|
||||||
|
public abstract class UpdateHandlerAttribute<T>(UpdateType updateType, int concurrency = 0)
|
||||||
|
: UpdateHandlerAttributeBase([typeof(T)], updateType, concurrency) where T : UpdateHandlerBase
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Telegrator.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for configuring handler collection behavior.
|
||||||
|
/// Defines options that control how handlers are collected and processed during initialization.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHandlersCollectingOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to descend the indexr of handler's index on register. ('false' by default)
|
||||||
|
/// </summary>
|
||||||
|
public bool DescendDescriptorIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to exclude intersecting command aliases.
|
||||||
|
/// </summary>
|
||||||
|
public bool ExceptIntersectingCommandAliases { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
|
namespace Telegrator.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for providing bot information and metadata.
|
||||||
|
/// Contains information about the bot user and provides initialization capabilities.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITelegramBotInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="User"/> representing the bot.
|
||||||
|
/// </summary>
|
||||||
|
public User User { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace Telegrator.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for Telegram bot behavior and execution settings.
|
||||||
|
/// Controls various aspects of bot operation including concurrency, routing, and execution policies.
|
||||||
|
/// </summary>
|
||||||
|
public class TelegramBotOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether only the first found handler should be executed for each update.
|
||||||
|
/// </summary>
|
||||||
|
public bool ExecuteOnlyFirstFoundHanlder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum number of parallel working handlers. Null means no limit.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaximumParallelWorkingHandlers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether awaiting handlers should be routed separately from regular handlers.
|
||||||
|
/// </summary>
|
||||||
|
public bool ExclusiveAwaitingHandlerRouting { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the global cancellation token for all bot operations.
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken GlobalCancellationToken { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
namespace Telegrator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enumeration of dice types supported by Telegram.
|
||||||
|
/// Used for filtering dice messages and determining dice emoji representations.
|
||||||
|
/// </summary>
|
||||||
|
public enum DiceType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Standard dice (🎲).
|
||||||
|
/// </summary>
|
||||||
|
Dice,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Darts (🎯).
|
||||||
|
/// </summary>
|
||||||
|
Darts,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bowling (🎳).
|
||||||
|
/// </summary>
|
||||||
|
Bowling,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Basketball (🏀).
|
||||||
|
/// </summary>
|
||||||
|
Basketball,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Football (⚽).
|
||||||
|
/// </summary>
|
||||||
|
Football,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Casino slot machine (🎰).
|
||||||
|
/// </summary>
|
||||||
|
Casino
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using Telegrator.MadiatorCore.Descriptors;
|
||||||
|
|
||||||
|
namespace Telegrator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when attempting to modify a frozen collection.
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionFrozenException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CollectionFrozenException"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public CollectionFrozenException()
|
||||||
|
: base("Can't change a frozen collection.") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a type is not a valid filter type.
|
||||||
|
/// </summary>
|
||||||
|
public class NotFilterTypeException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="NotFilterTypeException"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type that is not a filter type.</param>
|
||||||
|
public NotFilterTypeException(Type type)
|
||||||
|
: base(string.Format("\"{0}\" is not a filter type", type.Name)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a handler execution fails.
|
||||||
|
/// Contains information about the handler and the inner exception.
|
||||||
|
/// </summary>
|
||||||
|
public class HandlerFaultedException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The handler info associated with the faulted handler.
|
||||||
|
/// </summary>
|
||||||
|
public readonly DescribedHandlerInfo HandlerInfo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HandlerFaultedException"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handlerInfo">The handler info.</param>
|
||||||
|
/// <param name="inner">The inner exception.</param>
|
||||||
|
public HandlerFaultedException(DescribedHandlerInfo handlerInfo, Exception inner)
|
||||||
|
: base(string.Format("Handler's \"{0}\" execution was faulted", handlerInfo.DisplayString), inner)
|
||||||
|
{
|
||||||
|
HandlerInfo = handlerInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
using Telegrator.Handlers;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if a command matches any of the specified aliases.
|
||||||
|
/// Requires a <see cref="CommandHandlerAttribute"/> to be applied first to extract the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="alliases">The command aliases to check against.</param>
|
||||||
|
public class CommandAlliasFilter(params string[] alliases) : Filter<Message>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the command that was received and extracted by the <see cref="CommandHandlerAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string ReceivedCommand { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the received command matches any of the specified aliases.
|
||||||
|
/// This filter requires a <see cref="CommandHandlerAttribute"/> to be applied first
|
||||||
|
/// to extract the command from the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context containing the completed filters.</param>
|
||||||
|
/// <returns>True if the command matches any of the specified aliases; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> context)
|
||||||
|
{
|
||||||
|
ReceivedCommand = context.CompletedFilters.Get<CommandHandlerAttribute>(0).ReceivedCommand;
|
||||||
|
return alliases.Contains(ReceivedCommand, StringComparer.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Telegram.Bot;
|
||||||
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a compiled filter that applies a set of filters to an anonymous target type.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnonymousCompiledFilter : AnonymousTypeFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AnonymousCompiledFilter"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filterAction">The filter action delegate.</param>
|
||||||
|
/// <param name="getFilterringTarget">The function to get the filtering target from an update.</param>
|
||||||
|
private AnonymousCompiledFilter(Func<FilterExecutionContext<Update>, object, bool> filterAction, Func<Update, object?> getFilterringTarget)
|
||||||
|
: base(filterAction, getFilterringTarget) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compiles a set of filters into an <see cref="AnonymousCompiledFilter"/> for a specific target type.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the filtering target.</typeparam>
|
||||||
|
/// <param name="filters">The list of filters to compile.</param>
|
||||||
|
/// <param name="getFilterringTarget">The function to get the filtering target from an update.</param>
|
||||||
|
/// <returns>The compiled filter.</returns>
|
||||||
|
public static AnonymousCompiledFilter Compile<T>(IList<IFilter<T>> filters, Func<Update, object?> getFilterringTarget) where T : class
|
||||||
|
{
|
||||||
|
return new AnonymousCompiledFilter(
|
||||||
|
(context, filterringTarget) => CanPassInternal(filters, context, filterringTarget),
|
||||||
|
getFilterringTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether all filters can pass for the given context and filtering target.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the filtering target.</typeparam>
|
||||||
|
/// <param name="filters">The list of filters.</param>
|
||||||
|
/// <param name="updateContext">The filter execution context.</param>
|
||||||
|
/// <param name="filterringTarget">The filtering target.</param>
|
||||||
|
/// <returns>True if all filters pass; otherwise, false.</returns>
|
||||||
|
private static bool CanPassInternal<T>(IList<IFilter<T>> filters, FilterExecutionContext<Update> updateContext, object filterringTarget) where T : class
|
||||||
|
{
|
||||||
|
FilterExecutionContext<T> context = updateContext.CreateChild((T)filterringTarget);
|
||||||
|
|
||||||
|
foreach (IFilter<T> filter in filters)
|
||||||
|
{
|
||||||
|
if (!filter.CanPass(context))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
context.CompletedFilters.Add(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a filter that applies a filter action to an anonymous target type extracted from an update.
|
||||||
|
/// </summary>
|
||||||
|
public class AnonymousTypeFilter : Filter<Update>
|
||||||
|
{
|
||||||
|
private readonly Func<FilterExecutionContext<Update>, object, bool> FilterAction;
|
||||||
|
private readonly Func<Update, object?> GetFilterringTarget;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AnonymousTypeFilter"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filterAction">The filter action delegate.</param>
|
||||||
|
/// <param name="getFilterringTarget">The function to get the filtering target from an update.</param>
|
||||||
|
protected AnonymousTypeFilter(Func<FilterExecutionContext<Update>, object, bool> filterAction, Func<Update, object?> getFilterringTarget)
|
||||||
|
{
|
||||||
|
FilterAction = filterAction;
|
||||||
|
GetFilterringTarget = getFilterringTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compiles a filter for a specific target type.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the filtering target.</typeparam>
|
||||||
|
/// <param name="filter">The filter to apply.</param>
|
||||||
|
/// <param name="getFilterringTarget">The function to get the filtering target from an update.</param>
|
||||||
|
/// <returns>The compiled filter.</returns>
|
||||||
|
public static AnonymousTypeFilter Compile<T>(IFilter<T> filter, Func<Update, T?> getFilterringTarget) where T : class
|
||||||
|
{
|
||||||
|
return new AnonymousTypeFilter(
|
||||||
|
(context, filterringTarget) => CanPassInternal(context, filter, filterringTarget),
|
||||||
|
getFilterringTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the filter can pass for the given context and filtering target.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the filtering target.</typeparam>
|
||||||
|
/// <param name="updateContext">The filter execution context.</param>
|
||||||
|
/// <param name="filter">The filter to apply.</param>
|
||||||
|
/// <param name="filterringTarget">The filtering target.</param>
|
||||||
|
/// <returns>True if the filter passes; otherwise, false.</returns>
|
||||||
|
private static bool CanPassInternal<T>(FilterExecutionContext<Update> updateContext, IFilter<T> filter, object filterringTarget) where T : class
|
||||||
|
{
|
||||||
|
FilterExecutionContext<T> context = updateContext.CreateChild((T)filterringTarget);
|
||||||
|
if (!filter.CanPass(context))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
context.CompletedFilters.Add(filter);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the filter can pass for the given context by extracting the filtering target and applying the filter action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context.</param>
|
||||||
|
/// <returns>True if the filter passes; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Update> context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object? filterringTarget = GetFilterringTarget.Invoke(context.Input);
|
||||||
|
if (filterringTarget == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return FilterAction.Invoke(context, filterringTarget);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
namespace Telegrator.Filters.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a filter that composes multiple filters and passes only if all of them pass.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public class CompiledFilter<T> : Filter<T> where T : class
|
||||||
|
{
|
||||||
|
private readonly IFilter<T>[] Filters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CompiledFilter{T}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filters">The filters to compose.</param>
|
||||||
|
private CompiledFilter(IFilter<T>[] filters)
|
||||||
|
{
|
||||||
|
Filters = filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compiles multiple filters into a <see cref="CompiledFilter{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filters">The filters to compose.</param>
|
||||||
|
/// <returns>A new <see cref="CompiledFilter{T}"/> instance.</returns>
|
||||||
|
public static CompiledFilter<T> Compile(params IFilter<T>[] filters)
|
||||||
|
{
|
||||||
|
return new CompiledFilter<T>(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether all composed filters pass for the given context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context.</param>
|
||||||
|
/// <returns>True if all filters pass; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<T> context)
|
||||||
|
{
|
||||||
|
foreach (IFilter<T> filter in Filters)
|
||||||
|
{
|
||||||
|
if (!filter.CanPass(context))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
context.CompletedFilters.Add(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The list containing filters worked out during Polling to further obtain additional filtering information
|
||||||
|
/// </summary>
|
||||||
|
public class CompletedFiltersList : IEnumerable<IFilterCollectable>
|
||||||
|
{
|
||||||
|
private readonly List<IFilterCollectable> CompletedFilters = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the completed filter to the list.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TUpdate">The type of update.</typeparam>
|
||||||
|
/// <param name="filter">The filter to add.</param>
|
||||||
|
public void Add<TUpdate>(IFilter<TUpdate> filter) where TUpdate : class
|
||||||
|
{
|
||||||
|
if (filter is AnonymousTypeFilter | filter is AnonymousCompiledFilter)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!filter.IsCollectible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CompletedFilters.Add(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds many completed filters to the list.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TUpdate">The type of update.</typeparam>
|
||||||
|
/// <param name="filters">The filters to add.</param>
|
||||||
|
public void AddRange<TUpdate>(IEnumerable<IFilter<TUpdate>> filters) where TUpdate : class
|
||||||
|
{
|
||||||
|
foreach (IFilter<TUpdate> filter in filters)
|
||||||
|
Add(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks for filters of a given type in the list.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TFilter">The filter type to search for.</typeparam>
|
||||||
|
/// <returns>The enumerable containing filters of the given type.</returns>
|
||||||
|
/// <exception cref="NotFilterTypeException">Thrown if the type is not a filter type.</exception>
|
||||||
|
public IEnumerable<TFilter> Get<TFilter>() where TFilter : notnull, IFilterCollectable
|
||||||
|
{
|
||||||
|
if (!typeof(TFilter).IsFilterType())
|
||||||
|
throw new NotFilterTypeException(typeof(TFilter));
|
||||||
|
|
||||||
|
return CompletedFilters.WhereCast<TFilter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks for a filter of a given type at the specified index in the list.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TFilter">The filter type to search for.</typeparam>
|
||||||
|
/// <param name="index">The index of the filter.</param>
|
||||||
|
/// <returns>The filter of the given type at the specified index.</returns>
|
||||||
|
/// <exception cref="NotFilterTypeException">Thrown if the type is not a filter type.</exception>
|
||||||
|
/// <exception cref="KeyNotFoundException">Thrown if no filter is found at the index.</exception>
|
||||||
|
public TFilter Get<TFilter>(int index) where TFilter : notnull, IFilterCollectable
|
||||||
|
{
|
||||||
|
IEnumerable<TFilter> filters = Get<TFilter>();
|
||||||
|
return filters.Any() ? filters.ElementAt(index) : throw new KeyNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a filter of a given type at the specified index, or null if it does not exist.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TFilter">The filter type to search for.</typeparam>
|
||||||
|
/// <param name="index">The index of the filter.</param>
|
||||||
|
/// <returns>The filter at the specified index, or null if it does not exist.</returns>
|
||||||
|
/// <exception cref="NotFilterTypeException">Thrown if the type is not a filter type.</exception>
|
||||||
|
public TFilter? GetOrDefault<TFilter>(int index) where TFilter : IFilterCollectable
|
||||||
|
{
|
||||||
|
IEnumerable<TFilter> filters = Get<TFilter>();
|
||||||
|
return filters.Any() ? filters.ElementAt(index) : default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IEnumerator<IFilterCollectable> GetEnumerator() => CompletedFilters.GetEnumerator();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => CompletedFilters.GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Configuration;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the context for filter execution, including update, input, and additional data.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public class FilterExecutionContext<T> where T : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="ITelegramBotInfo"/> for the current context.
|
||||||
|
/// </summary>
|
||||||
|
public ITelegramBotInfo BotInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the additional data dictionary for the context.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object> Data { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of completed filters for the context.
|
||||||
|
/// </summary>
|
||||||
|
public CompletedFiltersList CompletedFilters { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="Update"/> being processed.
|
||||||
|
/// </summary>
|
||||||
|
public Update Update { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="UpdateType"/> of the update.
|
||||||
|
/// </summary>
|
||||||
|
public UpdateType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the input object for the filter.
|
||||||
|
/// </summary>
|
||||||
|
public T Input { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FilterExecutionContext{T}"/> class with all parameters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="botInfo">The bot info.</param>
|
||||||
|
/// <param name="update">The update.</param>
|
||||||
|
/// <param name="input">The input object.</param>
|
||||||
|
/// <param name="data">The additional data dictionary.</param>
|
||||||
|
/// <param name="completedFilters">The list of completed filters.</param>
|
||||||
|
public FilterExecutionContext(ITelegramBotInfo botInfo, Update update, T input, Dictionary<string, object> data, CompletedFiltersList completedFilters)
|
||||||
|
{
|
||||||
|
BotInfo = botInfo;
|
||||||
|
Data = data;
|
||||||
|
CompletedFilters = completedFilters;
|
||||||
|
Update = update;
|
||||||
|
Type = update.Type;
|
||||||
|
Input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FilterExecutionContext{T}"/> class with default data and filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="botInfo">The bot info.</param>
|
||||||
|
/// <param name="update">The update.</param>
|
||||||
|
/// <param name="input">The input object.</param>
|
||||||
|
public FilterExecutionContext(ITelegramBotInfo botInfo, Update update, T input)
|
||||||
|
: this(botInfo, update, input, [], []) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a child context for a different input type, sharing the same data and completed filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="C">The type of the new input.</typeparam>
|
||||||
|
/// <param name="input">The new input object.</param>
|
||||||
|
/// <returns>A new <see cref="FilterExecutionContext{C}"/> instance.</returns>
|
||||||
|
public FilterExecutionContext<C> CreateChild<C>(C input) where C : class
|
||||||
|
=> new FilterExecutionContext<C>(BotInfo, Update, input, Data, CompletedFilters);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Telegrator.Filters.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for filters that can be collected into a completed filters list.
|
||||||
|
/// Provides information about whether a filter should be tracked during execution.
|
||||||
|
/// </summary>
|
||||||
|
public interface IFilterCollectable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets if filter can be collected to <see cref="CompletedFiltersList"/>
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCollectible { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a filter for a specific update type.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the update to filter.</typeparam>
|
||||||
|
public interface IFilter<T> : IFilterCollectable where T : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the filter can pass for the given context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info">The filter execution context.</param>
|
||||||
|
/// <returns>True if the filter passes; otherwise, false.</returns>
|
||||||
|
public bool CanPass(FilterExecutionContext<T> info);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Telegrator.Filters.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a filter that joins multiple filters together.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public interface IJoinedFilter<T> : IFilter<T> where T : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the array of joined filters.
|
||||||
|
/// </summary>
|
||||||
|
public IFilter<T>[] Filters { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base class for filters that operate based on the current environment.
|
||||||
|
/// Provides functionality to detect debug vs release environments.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class EnvironmentFilter : Filter<Update>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the current environment is debug mode.
|
||||||
|
/// This is set during static initialization based on the DEBUG conditional compilation symbol.
|
||||||
|
/// </summary>
|
||||||
|
protected static bool IsCurrentEnvDebug { get; private set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static constructor that initializes the environment detection.
|
||||||
|
/// </summary>
|
||||||
|
static EnvironmentFilter()
|
||||||
|
=> SetIsCurrentEnvDebug();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the debug environment flag. This method is only compiled in DEBUG builds.
|
||||||
|
/// </summary>
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void SetIsCurrentEnvDebug()
|
||||||
|
=> IsCurrentEnvDebug = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that only passes in debug environment builds.
|
||||||
|
/// </summary>
|
||||||
|
public class IsDebugEnvironmentFilter() : EnvironmentFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the current environment is debug mode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the current environment is debug mode; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Update> _)
|
||||||
|
=> IsCurrentEnvDebug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that only passes in release environment builds.
|
||||||
|
/// </summary>
|
||||||
|
public class IsReleaseEnvironmentFilter() : EnvironmentFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the current environment is release mode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the current environment is release mode; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Update> _)
|
||||||
|
=> !IsCurrentEnvDebug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks environment variable values.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="variable">The environment variable name to check.</param>
|
||||||
|
/// <param name="value">The expected value of the environment variable (optional).</param>
|
||||||
|
/// <param name="comparison">The string comparison type to use for value matching.</param>
|
||||||
|
public class EnvironmentVariableFilter(string variable, string? value, StringComparison comparison) : Filter<Update>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The environment variable name to check.
|
||||||
|
/// </summary>
|
||||||
|
private readonly string _variable = variable;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The expected value of the environment variable (optional).
|
||||||
|
/// </summary>
|
||||||
|
private readonly string? _value = value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The string comparison type to use for value matching.
|
||||||
|
/// </summary>
|
||||||
|
private readonly StringComparison _comparison = comparison;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="EnvironmentVariableFilter"/> class with a specific value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="variable">The environment variable name to check.</param>
|
||||||
|
/// <param name="value">The expected value of the environment variable.</param>
|
||||||
|
public EnvironmentVariableFilter(string variable, string? value)
|
||||||
|
: this(variable, value, StringComparison.InvariantCulture) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="EnvironmentVariableFilter"/> class that checks for non-null values.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="variable">The environment variable name to check.</param>
|
||||||
|
public EnvironmentVariableFilter(string variable)
|
||||||
|
: this(variable, "{NOT_NULL}", StringComparison.InvariantCulture) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="EnvironmentVariableFilter"/> class with custom comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="variable">The environment variable name to check.</param>
|
||||||
|
/// <param name="comparison">The string comparison type to use.</param>
|
||||||
|
public EnvironmentVariableFilter(string variable, StringComparison comparison)
|
||||||
|
: this(variable, "{NOT_NULL}", comparison) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the environment variable matches the expected criteria.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the environment variable matches the criteria; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Update> _)
|
||||||
|
{
|
||||||
|
string? envValue = Environment.GetEnvironmentVariable(_variable);
|
||||||
|
|
||||||
|
if (envValue == null && _value == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (envValue == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return envValue.Equals(_value, _comparison);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using Telegrator;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for filters, providing logical operations and collectability.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public abstract class Filter<T> : IFilter<T> where T : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a filter from a function.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">The filter function.</param>
|
||||||
|
/// <returns>A <see cref="Filter{T}"/> instance.</returns>
|
||||||
|
public static Filter<T> If(Func<FilterExecutionContext<T>, bool> filter)
|
||||||
|
=> new FunctionFilter<T>(filter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a filter that always passes.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An <see cref="AnyFilter{T}"/> instance.</returns>
|
||||||
|
public static AnyFilter<T> Any()
|
||||||
|
=> new AnyFilter<T>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a filter that inverts the result of this filter.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="ReverseFilter{T}"/> instance.</returns>
|
||||||
|
public Filter<T> Not()
|
||||||
|
=> new ReverseFilter<T>(this);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a filter that passes only if both this and the specified filter pass.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">The filter to combine with.</param>
|
||||||
|
/// <returns>An <see cref="AndFilter{T}"/> instance.</returns>
|
||||||
|
public AndFilter<T> And(IFilter<T> filter)
|
||||||
|
=> new AndFilter<T>(this, filter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a filter that passes if either this or the specified filter pass.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">The filter to combine with.</param>
|
||||||
|
/// <returns>An <see cref="OrFilter{T}"/> instance.</returns>
|
||||||
|
public OrFilter<T> Or(IFilter<T> filter)
|
||||||
|
=> new OrFilter<T>(this, filter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this filter is collectible.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCollectible => this.HasPublicProperties();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the filter can pass for the given context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context.</param>
|
||||||
|
/// <returns>True if the filter passes; otherwise, false.</returns>
|
||||||
|
public abstract bool CanPass(FilterExecutionContext<T> context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A filter that always passes.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public class AnyFilter<T> : Filter<T> where T : class
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<T> context)
|
||||||
|
=> true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A filter that inverts the result of another filter.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public class ReverseFilter<T> : Filter<T> where T : class
|
||||||
|
{
|
||||||
|
private readonly IFilter<T> filter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ReverseFilter{T}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">The filter to invert.</param>
|
||||||
|
public ReverseFilter(IFilter<T> filter)
|
||||||
|
=> this.filter = filter;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<T> context)
|
||||||
|
=> !filter.CanPass(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A filter that uses a function to determine if it passes.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public class FunctionFilter<T> : Filter<T> where T : class
|
||||||
|
{
|
||||||
|
private readonly Func<FilterExecutionContext<T>, bool>? FilterFunc;
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FunctionFilter{T}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="funcFilter">The filter function.</param>
|
||||||
|
public FunctionFilter(Func<FilterExecutionContext<T>, bool> funcFilter)
|
||||||
|
=> FilterFunc = funcFilter;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<T> context)
|
||||||
|
=> context.Input != null && FilterFunc != null && FilterFunc(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for filters that join multiple filters together.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public abstract class JoinedFilter<T>(params IFilter<T>[] filters) : Filter<T>, IJoinedFilter<T> where T : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the array of joined filters.
|
||||||
|
/// </summary>
|
||||||
|
public IFilter<T>[] Filters { get; } = filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A filter that passes only if both joined filters pass.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public class AndFilter<T> : JoinedFilter<T> where T : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AndFilter{T}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="leftFilter">The left filter.</param>
|
||||||
|
/// <param name="rightFilter">The right filter.</param>
|
||||||
|
public AndFilter(IFilter<T> leftFilter, IFilter<T> rightFilter)
|
||||||
|
: base(leftFilter, rightFilter) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<T> context)
|
||||||
|
=> Filters[0].CanPass(context) && Filters[1].CanPass(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A filter that passes if at least one of the joined filters passes.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public class OrFilter<T> : JoinedFilter<T> where T : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="OrFilter{T}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="leftFilter">The left filter.</param>
|
||||||
|
/// <param name="rightFilter">The right filter.</param>
|
||||||
|
public OrFilter(IFilter<T> leftFilter, IFilter<T> rightFilter)
|
||||||
|
: base(leftFilter, rightFilter) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<T> context)
|
||||||
|
=> Filters[0].CanPass(context) || Filters[1].CanPass(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if a message contains a mention of the bot or a specific user.
|
||||||
|
/// Requires a <see cref="MessageHasEntityFilter"/> to be applied first to identify mention entities.
|
||||||
|
/// </summary>
|
||||||
|
public class MentionedFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The username to check for in the mention (null means check for bot's username).
|
||||||
|
/// </summary>
|
||||||
|
private readonly string? Mention;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MentionedFilter"/> class that checks for bot mentions.
|
||||||
|
/// </summary>
|
||||||
|
public MentionedFilter()
|
||||||
|
{
|
||||||
|
Mention = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MentionedFilter"/> class that checks for specific user mentions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mention">The username to check for in the mention.</param>
|
||||||
|
public MentionedFilter(string mention)
|
||||||
|
{
|
||||||
|
Mention = mention;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message contains a mention of the specified user or bot.
|
||||||
|
/// This filter requires a <see cref="MessageHasEntityFilter"/> to be applied first
|
||||||
|
/// to identify mention entities in the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context containing the message and completed filters.</param>
|
||||||
|
/// <returns>True if the message contains the specified mention; otherwise, false.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when the bot username is null and no specific mention is provided.</exception>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> 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<MessageHasEntityFilter>(0);
|
||||||
|
return entityFilter.FoundEntities.Any(ent => context.Input.Text.Substring(ent.Offset + 1, ent.Length - 1) == userName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for filters that operate on the chat of the message being processed.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class MessageChatFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the chat of the message being processed.
|
||||||
|
/// </summary>
|
||||||
|
public Chat Chat { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> context)
|
||||||
|
{
|
||||||
|
Chat = context.Input.Chat;
|
||||||
|
return CanPassNext(context.CreateChild(Chat));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the filter passes for the given chat context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context for the chat.</param>
|
||||||
|
/// <returns>True if the filter passes; otherwise, false.</returns>
|
||||||
|
protected abstract bool CanPassNext(FilterExecutionContext<Chat> _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages whose chat is a forum.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageChatIsForumFilter : MessageChatFilter
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Chat> _)
|
||||||
|
=> Chat.IsForum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages whose chat ID matches the specified value.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageChatIdFilter(long id) : MessageChatFilter
|
||||||
|
{
|
||||||
|
private readonly long Id = id;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Chat> _)
|
||||||
|
=> Chat.Id == Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages whose chat type matches the specified value.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageChatTypeFilter(ChatType type) : MessageChatFilter
|
||||||
|
{
|
||||||
|
private readonly ChatType Type = type;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Chat> _)
|
||||||
|
=> Chat.Type == Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages whose chat title matches the specified value.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageChatTitleFilter : MessageChatFilter
|
||||||
|
{
|
||||||
|
private readonly string? Title;
|
||||||
|
private readonly StringComparison Comparison = StringComparison.InvariantCulture;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageChatTitleFilter"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The chat title to match.</param>
|
||||||
|
public MessageChatTitleFilter(string? title) => Title = title;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageChatTitleFilter"/> class with a specific string comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The chat title to match.</param>
|
||||||
|
/// <param name="comparison">The string comparison to use.</param>
|
||||||
|
public MessageChatTitleFilter(string? title, StringComparison comparison)
|
||||||
|
: this(title) => Comparison = comparison;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Chat> _)
|
||||||
|
{
|
||||||
|
if (Chat.Title == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Chat.Title.Equals(Title, Comparison);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages whose chat username matches the specified value.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageChatUsernameFilter : MessageChatFilter
|
||||||
|
{
|
||||||
|
private readonly string? UserName;
|
||||||
|
private readonly StringComparison Comparison = StringComparison.InvariantCulture;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageChatUsernameFilter"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userName">The chat username to match.</param>
|
||||||
|
public MessageChatUsernameFilter(string? userName) => UserName = userName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageChatUsernameFilter"/> class with a specific string comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userName">The chat username to match.</param>
|
||||||
|
/// <param name="comparison">The string comparison to use.</param>
|
||||||
|
public MessageChatUsernameFilter(string? userName, StringComparison comparison)
|
||||||
|
: this(userName) => Comparison = comparison;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Chat> _)
|
||||||
|
{
|
||||||
|
if (Chat.Username == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Chat.Username.Equals(UserName, Comparison);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages whose chat first and/or last name matches the specified values.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageChatNameFilter : MessageChatFilter
|
||||||
|
{
|
||||||
|
private readonly string? FirstName;
|
||||||
|
private readonly string? LastName;
|
||||||
|
private readonly StringComparison Comparison = StringComparison.InvariantCulture;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageChatNameFilter"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The chat first name to match.</param>
|
||||||
|
/// <param name="lastName">The chat last name to match.</param>
|
||||||
|
public MessageChatNameFilter(string? firstName, string? lastName)
|
||||||
|
{
|
||||||
|
FirstName = firstName;
|
||||||
|
LastName = lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageChatNameFilter"/> class with a specific string comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The chat first name to match.</param>
|
||||||
|
/// <param name="lastName">The chat last name to match.</param>
|
||||||
|
/// <param name="comparison">The string comparison to use.</param>
|
||||||
|
public MessageChatNameFilter(string? firstName, string? lastName, StringComparison comparison)
|
||||||
|
: this(firstName, lastName) => Comparison = comparison;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Chat> _)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages by their <see cref="MessageType"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageTypeFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
private readonly MessageType type;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageTypeFilter"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The message type to filter by.</param>
|
||||||
|
public MessageTypeFilter(MessageType type) => this.type = type;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> context)
|
||||||
|
=> context.Input.Type == type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages that are automatic forwards.
|
||||||
|
/// </summary>
|
||||||
|
public class IsAutomaticFormwardMessageFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> context)
|
||||||
|
=> context.Input.IsAutomaticForward;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages that are sent from offline.
|
||||||
|
/// </summary>
|
||||||
|
public class IsFromOfflineMessageFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> context)
|
||||||
|
=> context.Input.IsFromOffline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters service messages (e.g., chat events).
|
||||||
|
/// </summary>
|
||||||
|
public class IsServiceMessageMessageFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> context)
|
||||||
|
=> context.Input.IsServiceMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages that are topic messages.
|
||||||
|
/// </summary>
|
||||||
|
public class IsTopicMessageMessageFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> context)
|
||||||
|
=> context.Input.IsTopicMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages by dice throw value and optionally by dice type.
|
||||||
|
/// </summary>
|
||||||
|
public class DiceThrowedFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
private readonly DiceType? Dice;
|
||||||
|
private readonly int Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DiceThrowedFilter"/> class for a specific value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The dice value to filter by.</param>
|
||||||
|
public DiceThrowedFilter(int value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DiceThrowedFilter"/> class for a specific dice type and value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="diceType">The dice type to filter by.</param>
|
||||||
|
/// <param name="value">The dice value to filter by.</param>
|
||||||
|
public DiceThrowedFilter(DiceType diceType, int value) : this(value) => Dice = diceType;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages by matching their text with a regular expression.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageRegexFilter : RegexFilterBase<Message>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageRegexFilter"/> class with a pattern and options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pattern">The regex pattern.</param>
|
||||||
|
/// <param name="regexOptions">The regex options.</param>
|
||||||
|
public MessageRegexFilter(string pattern, RegexOptions regexOptions = default)
|
||||||
|
: base(msg => msg.Text, pattern, regexOptions) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageRegexFilter"/> class with a regex object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="regex">The regex object.</param>
|
||||||
|
public MessageRegexFilter(Regex regex)
|
||||||
|
: base(msg => msg.Text, regex) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters messages that contain a specific entity type, content, offset, or length.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageHasEntityFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
private readonly StringComparison _stringComparison = StringComparison.CurrentCulture;
|
||||||
|
private readonly MessageEntityType? EntityType;
|
||||||
|
private readonly string? Content;
|
||||||
|
private readonly int? Offset;
|
||||||
|
private readonly int? Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the entities found in the message that match the filter.
|
||||||
|
/// </summary>
|
||||||
|
public MessageEntity[] FoundEntities { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageHasEntityFilter"/> class for a specific entity type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to filter by.</param>
|
||||||
|
public MessageHasEntityFilter(MessageEntityType type)
|
||||||
|
{
|
||||||
|
EntityType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageHasEntityFilter"/> class for a specific entity type, offset, and length.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to filter by.</param>
|
||||||
|
/// <param name="offset">The offset to filter by.</param>
|
||||||
|
/// <param name="length">The length to filter by.</param>
|
||||||
|
public MessageHasEntityFilter(MessageEntityType type, int offset, int? length)
|
||||||
|
{
|
||||||
|
EntityType = type;
|
||||||
|
Offset = offset;
|
||||||
|
Length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageHasEntityFilter"/> class for a specific entity type and content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to filter by.</param>
|
||||||
|
/// <param name="content">The content to filter by.</param>
|
||||||
|
/// <param name="stringComparison">The string comparison to use.</param>
|
||||||
|
public MessageHasEntityFilter(MessageEntityType type, string content, StringComparison stringComparison = StringComparison.CurrentCulture)
|
||||||
|
{
|
||||||
|
EntityType = type;
|
||||||
|
Content = content;
|
||||||
|
_stringComparison = stringComparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MessageHasEntityFilter"/> class for a specific entity type, offset, length, and content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The entity type to filter by.</param>
|
||||||
|
/// <param name="offset">The offset to filter by.</param>
|
||||||
|
/// <param name="length">The length to filter by.</param>
|
||||||
|
/// <param name="content">The content to filter by.</param>
|
||||||
|
/// <param name="stringComparison">The string comparison to use.</param>
|
||||||
|
public MessageHasEntityFilter(MessageEntityType type, int offset, int? length, string content, StringComparison stringComparison = StringComparison.CurrentCulture)
|
||||||
|
{
|
||||||
|
EntityType = type;
|
||||||
|
Offset = offset;
|
||||||
|
Length = length;
|
||||||
|
Content = content;
|
||||||
|
_stringComparison = stringComparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base class for filters that operate on message senders.
|
||||||
|
/// Provides functionality to access and validate the user who sent the message.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class MessageSenderFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user who sent the message.
|
||||||
|
/// </summary>
|
||||||
|
public User User { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the message can pass through the filter by validating
|
||||||
|
/// that the message has a valid sender.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context containing the message.</param>
|
||||||
|
/// <returns>True if the message has a valid sender; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> context)
|
||||||
|
{
|
||||||
|
User = context.Input.From!;
|
||||||
|
if (User is not { Id: > 0 })
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return CanPassNext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract method that must be implemented by derived classes to perform
|
||||||
|
/// specific filtering logic on the message sender.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context.</param>
|
||||||
|
/// <returns>True if the sender passes the specific filter criteria; otherwise, false.</returns>
|
||||||
|
protected abstract bool CanPassNext(FilterExecutionContext<Message> context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message sender has a specific username.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username to check for.</param>
|
||||||
|
public class FromUsernameFilter(string username) : MessageSenderFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The username to check for.
|
||||||
|
/// </summary>
|
||||||
|
private readonly string _username = username;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The string comparison type to use for username matching.
|
||||||
|
/// </summary>
|
||||||
|
private readonly StringComparison _comparison = StringComparison.InvariantCulture;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FromUsernameFilter"/> class with custom string comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username to check for.</param>
|
||||||
|
/// <param name="comparison">The string comparison type to use.</param>
|
||||||
|
public FromUsernameFilter(string username, StringComparison comparison)
|
||||||
|
: this(username) => _comparison = comparison;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message sender has the specified username.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the sender has the specified username; otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> context)
|
||||||
|
=> User.Username != null && User.Username.Equals(_username, _comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message sender has specific first and/or last name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to check for.</param>
|
||||||
|
/// <param name="lastName">The last name to check for (optional).</param>
|
||||||
|
/// <param name="comparison">The string comparison type to use.</param>
|
||||||
|
public class FromUserFilter(string firstName, string? lastName, StringComparison comparison) : MessageSenderFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The first name to check for.
|
||||||
|
/// </summary>
|
||||||
|
private readonly string _firstName = firstName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last name to check for (optional).
|
||||||
|
/// </summary>
|
||||||
|
private readonly string? _lastName = lastName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The string comparison type to use for name matching.
|
||||||
|
/// </summary>
|
||||||
|
private readonly StringComparison _comparison = comparison;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FromUserFilter"/> class with first and last name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to check for.</param>
|
||||||
|
/// <param name="lastName">The last name to check for.</param>
|
||||||
|
public FromUserFilter(string firstName, string lastName)
|
||||||
|
: this(firstName, lastName, StringComparison.InvariantCulture) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FromUserFilter"/> class with first name only.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to check for.</param>
|
||||||
|
public FromUserFilter(string firstName)
|
||||||
|
: this(firstName, null, StringComparison.InvariantCulture) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FromUserFilter"/> class with first name and custom comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="firstName">The first name to check for.</param>
|
||||||
|
/// <param name="comparison">The string comparison type to use.</param>
|
||||||
|
public FromUserFilter(string firstName, StringComparison comparison)
|
||||||
|
: this(firstName, null, comparison) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message sender has the specified first and/or last name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the sender has the specified name(s); otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> context)
|
||||||
|
{
|
||||||
|
if (User.LastName != null)
|
||||||
|
{
|
||||||
|
if (_lastName == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_firstName.Equals(User.LastName, _comparison))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return User.FirstName.Equals(_firstName, _comparison);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message sender has a specific user ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user ID to check for.</param>
|
||||||
|
public class FromUserIdFilter(long userId) : MessageSenderFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user ID to check for.
|
||||||
|
/// </summary>
|
||||||
|
private readonly long _userId = userId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message sender has the specified user ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the sender has the specified user ID; otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> _)
|
||||||
|
=> User.Id == _userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message was sent by a bot.
|
||||||
|
/// </summary>
|
||||||
|
public class FromBotFilter() : MessageSenderFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message was sent by a bot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the message was sent by a bot; otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> _)
|
||||||
|
=> User.IsBot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message was sent by a premium user.
|
||||||
|
/// </summary>
|
||||||
|
public class FromPremiumUserFilter() : MessageSenderFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message was sent by a premium user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the message was sent by a premium user; otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> _)
|
||||||
|
=> User.IsPremium;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base class for filters that operate on message text content.
|
||||||
|
/// Provides common functionality for extracting and validating message text.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class MessageTextFilter : Filter<Message>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current message being processed by the filter.
|
||||||
|
/// </summary>
|
||||||
|
public Message Message { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the extracted text content from the current message.
|
||||||
|
/// </summary>
|
||||||
|
public string Text { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the message can pass through the filter by validating the message
|
||||||
|
/// and extracting its text content for further processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context containing the message update.</param>
|
||||||
|
/// <returns>True if the message is valid and can be processed further; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<Message> context)
|
||||||
|
{
|
||||||
|
Message = context.Update.Message!;
|
||||||
|
if (Message is not { Id: > 0 })
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Text = Message.Text ?? string.Empty;
|
||||||
|
return CanPassNext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract method that must be implemented by derived classes to perform
|
||||||
|
/// specific text-based filtering logic.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused in this context).</param>
|
||||||
|
/// <returns>True if the text content passes the filter criteria; otherwise, false.</returns>
|
||||||
|
protected abstract bool CanPassNext(FilterExecutionContext<Message> _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message text starts with a specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The content to check if the message text starts with.</param>
|
||||||
|
/// <param name="comparison">The string comparison type to use for the check.</param>
|
||||||
|
public class TextStartsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture) : MessageTextFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The content to check if the message text starts with.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly string Content = content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The string comparison type to use for the check.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly StringComparison Comparison = comparison;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message text starts with the specified content using the configured comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the text starts with the specified content; otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> _)
|
||||||
|
=> Text.StartsWith(Content, Comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message text ends with a specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The content to check if the message text ends with.</param>
|
||||||
|
/// <param name="comparison">The string comparison type to use for the check.</param>
|
||||||
|
public class TextEndsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture) : MessageTextFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The content to check if the message text ends with.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly string Content = content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The string comparison type to use for the check.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly StringComparison Comparison = comparison;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message text ends with the specified content using the configured comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the text ends with the specified content; otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> _)
|
||||||
|
=> Text.EndsWith(Content, Comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message text contains a specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The content to check if the message text contains.</param>
|
||||||
|
/// <param name="comparison">The string comparison type to use for the check.</param>
|
||||||
|
public class TextContainsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture) : MessageTextFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The content to check if the message text contains.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly string Content = content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The string comparison type to use for the check.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly StringComparison Comparison = comparison;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message text contains the specified content using the configured comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the text contains the specified content; otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> _)
|
||||||
|
=> Text.IndexOf(Content, Comparison) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message text equals a specified content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The content to check if the message text equals.</param>
|
||||||
|
/// <param name="comparison">The string comparison type to use for the check.</param>
|
||||||
|
public class TextEqualsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture) : MessageTextFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The content to check if the message text equals.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly string Content = content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The string comparison type to use for the check.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly StringComparison Comparison = comparison;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message text equals the specified content using the configured comparison.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the text equals the specified content; otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> _)
|
||||||
|
=> Text.Equals(Content, Comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if the message text is not null or empty.
|
||||||
|
/// </summary>
|
||||||
|
public class TextNotNullOrEmptyFilter() : MessageTextFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the message text is not null or empty.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="_">The filter execution context (unused).</param>
|
||||||
|
/// <returns>True if the text is not null or empty; otherwise, false.</returns>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> _)
|
||||||
|
=> !string.IsNullOrEmpty(Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for filters that use regular expressions to match text in updates.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the input for the filter.</typeparam>
|
||||||
|
public abstract class RegexFilterBase<T> : Filter<T> where T : class
|
||||||
|
{
|
||||||
|
private readonly Func<T, string?> getString;
|
||||||
|
private readonly Regex regex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection of matches found by the regex.
|
||||||
|
/// </summary>
|
||||||
|
public MatchCollection Matches { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RegexFilterBase{T}"/> class with a regex object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="getString">Function to extract the string to match from the input.</param>
|
||||||
|
/// <param name="regex">The regex object to use for matching.</param>
|
||||||
|
protected RegexFilterBase(Func<T, string?> getString, Regex regex)
|
||||||
|
{
|
||||||
|
this.getString = getString;
|
||||||
|
this.regex = regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RegexFilterBase{T}"/> class with a pattern and options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="getString">Function to extract the string to match from the input.</param>
|
||||||
|
/// <param name="pattern">The regex pattern.</param>
|
||||||
|
/// <param name="regexOptions">The regex options.</param>
|
||||||
|
protected RegexFilterBase(Func<T, string?> getString, string pattern, RegexOptions regexOptions = default)
|
||||||
|
{
|
||||||
|
this.getString = getString;
|
||||||
|
regex = new Regex(pattern, regexOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the regex matches the text extracted from the input.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context.</param>
|
||||||
|
/// <returns>True if the regex matches; otherwise, false.</returns>
|
||||||
|
public override bool CanPass(FilterExecutionContext<T> context)
|
||||||
|
{
|
||||||
|
string? text = getString.Invoke(context.Input);
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Matches = regex.Matches(text);
|
||||||
|
return Matches.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegrator.Filters.Components;
|
||||||
|
|
||||||
|
namespace Telegrator.Filters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter that checks if a replied message contains a mention of the bot or a specific user.
|
||||||
|
/// Requires a <see cref="MessageHasEntityFilter"/> to be applied first to identify mention entities.
|
||||||
|
/// </summary>
|
||||||
|
public class RepliedMentionedFilter : RepliedMessageFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The username to check for in the mention (null means check for bot's username).
|
||||||
|
/// </summary>
|
||||||
|
private readonly string? Mention;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RepliedMentionedFilter"/> class that checks for bot mentions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="replyDepth">The depth of reply chain to traverse (default: 1).</param>
|
||||||
|
public RepliedMentionedFilter(int replyDepth = 1) : base(replyDepth)
|
||||||
|
{
|
||||||
|
Mention = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RepliedMentionedFilter"/> class that checks for specific user mentions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mention">The username to check for in the mention.</param>
|
||||||
|
/// <param name="replyDepth">The depth of reply chain to traverse (default: 1).</param>
|
||||||
|
public RepliedMentionedFilter(string mention, int replyDepth = 1) : base(replyDepth)
|
||||||
|
{
|
||||||
|
Mention = mention;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the replied message contains a mention of the specified user or bot.
|
||||||
|
/// This filter requires a <see cref="MessageHasEntityFilter"/> to be applied first
|
||||||
|
/// to identify mention entities in the replied message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The filter execution context containing the message and completed filters.</param>
|
||||||
|
/// <returns>True if the replied message contains the specified mention; otherwise, false.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when the bot username is null and no specific mention is provided.</exception>
|
||||||
|
protected override bool CanPassNext(FilterExecutionContext<Message> 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<MessageHasEntityFilter>(0).FoundEntities.ElementAt(0);
|
||||||
|
|
||||||
|
string mention = Reply.Text.Substring(entity.Offset + 1, entity.Length - 1);
|
||||||
|
return userName == mention;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user