* Moved source code projects directory to 'src'

* Updatedt Solution format
This commit is contained in:
2026-03-06 21:12:21 +04:00
parent a18eab0f31
commit f1927cdda0
194 changed files with 10 additions and 83 deletions
@@ -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,17 @@
; 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
--------|----------|----------|-------
TLG201 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator
TLG202 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator
TLG203 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator
TLG204 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator
TLG205 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator
TLG206 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator
TLG207 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator
TLG101 | Telegrator.Design | Warning | DeveloperHelperAnalyzer
TLG102 | Telegrator.Design | Warning | DeveloperHelperAnalyzer
TLG103 | Telegrator.Design | Warning | DeveloperHelperAnalyzer
@@ -0,0 +1,200 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable;
using System.Text;
namespace Telegrator.Analyzers;
[Generator(LanguageNames.CSharp)]
public class DeveloperHelperAnalyzer : IIncrementalGenerator
{
private static readonly DiagnosticDescriptor MissingBaseClassWarning = new(
id: "TLG101",
title: "Missing handlers base class",
messageFormat: "Class '{0}' has attribute [{1}], but doesn't inherits {1}",
category: "Telegrator.Design",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor MissingAttributeWarning = new(
id: "TLG102",
title: "Missing handler annotation",
messageFormat: "Class '{0}' inherits '{1}', but doesn't have required annotation [{1}]",
category: "Telegrator.Design",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor MismatchedHandlerWarning = new(
id: "TLG103",
title: "Handlers Annotation and BaseClass mismatch",
messageFormat: "Class '{0}' has attribute [{1}], but inherits '{2}'",
category: "Telegrator.Design",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
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.Count == 0)
return false;
return true;
}
private static HandlerDeclarationModel Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
ClassDeclarationSyntax classSyntax = (ClassDeclarationSyntax)context.Node;
string? foundAttribute = classSyntax.GetHandlerAttributeName();
string? foundBaseClass = classSyntax.GetHandlerBaseClassName();
if (foundAttribute == null && foundBaseClass == null)
return null!;
string namespaceName = classSyntax.GetNamespace();
return new HandlerDeclarationModel(
classSyntax.Identifier.Text,
namespaceName,
foundAttribute,
foundBaseClass,
classSyntax.Identifier.GetLocation()
);
}
private static void Execute(SourceProductionContext context, ImmutableArray<HandlerDeclarationModel> handlers)
{
if (handlers.IsDefaultOrEmpty)
return;
List<MemberDeclarationSyntax> members = [];
foreach (HandlerDeclarationModel handler in handlers)
{
context.CancellationToken.ThrowIfCancellationRequested();
if (handler.AttributeName != null && handler.BaseClassName == null)
{
context.ReportDiagnostic(Diagnostic.Create(MissingBaseClassWarning, handler.Location, handler.ClassName, handler.AttributeName));
continue;
}
if (handler.AttributeName == null && handler.BaseClassName != null)
{
context.ReportDiagnostic(Diagnostic.Create(MissingAttributeWarning, handler.Location, handler.ClassName, handler.BaseClassName));
continue;
}
if (handler.AttributeName != handler.BaseClassName)
{
context.ReportDiagnostic(Diagnostic.Create(MismatchedHandlerWarning, handler.Location, handler.ClassName, handler.AttributeName, handler.BaseClassName));
continue;
}
FieldDeclarationSyntax fieldDeclaration = GenerateTypeField(handler);
members.Add(fieldDeclaration);
}
if (members.Count == 0)
return;
// 4. Сборка итогового файла
ClassDeclarationSyntax classDeclaration = SyntaxFactory.ClassDeclaration("AnalyzerExport")
.WithModifiers(SyntaxFactory.TokenList(
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
SyntaxFactory.Token(SyntaxKind.StaticKeyword),
SyntaxFactory.Token(SyntaxKind.PartialKeyword)))
.WithMembers(SyntaxFactory.List(members));
NamespaceDeclarationSyntax namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName("Telegrator.Analyzers"))
.WithMembers(SyntaxFactory.SingletonList<MemberDeclarationSyntax>(classDeclaration));
CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit()
.WithMembers(SyntaxFactory.SingletonList<MemberDeclarationSyntax>(namespaceDeclaration))
.NormalizeWhitespace();
SourceText sourceText = SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8);
context.AddSource("AnalyzerExport.g.cs", sourceText);
}
private static FieldDeclarationSyntax GenerateTypeField(HandlerDeclarationModel handler)
{
string fullTypeName = handler.Namespace == "Global"
? handler.ClassName
: $"{handler.Namespace}.{handler.ClassName}";
TypeOfExpressionSyntax typeofExpression = SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(fullTypeName));
VariableDeclaratorSyntax variableDeclarator = SyntaxFactory.VariableDeclarator(SyntaxFactory.Identifier($"{handler.ClassName}Type"))
.WithInitializer(SyntaxFactory.EqualsValueClause(typeofExpression));
return SyntaxFactory.FieldDeclaration(
SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName("System.Type"))
.WithVariables(SyntaxFactory.SingletonSeparatedList(variableDeclarator)))
.WithModifiers(SyntaxFactory.TokenList(
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
SyntaxFactory.Token(SyntaxKind.StaticKeyword),
SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)));
}
}
internal class HandlerDeclarationModel(string className, string namespaceName, string? attributeName, string? baseClassName, Location location)
{
public readonly string ClassName = className;
public readonly string Namespace = namespaceName;
public readonly string? AttributeName = attributeName;
public readonly string? BaseClassName = baseClassName;
public readonly Location Location = location;
}
internal static class DeveloperHelperAnalyzerExtensions
{
private static readonly string[] HandlersNames =
[
"AnyUpdateHandler",
"CallbackQueryHandler",
"CommandHandler",
"WelcomeHandler",
"MessageHandler"
];
// Ищет атрибут и возвращает его нормализованное имя (без суффикса Attribute)
public static string? GetHandlerAttributeName(this ClassDeclarationSyntax classSyntax)
{
string attributeName = classSyntax.AttributeLists
.SelectMany(list => list.Attributes)
.Select(attr => attr.Name.ToString())
.FirstOrDefault(name => HandlersNames.Any(h => name == h || name == h + "Attribute"));
return attributeName?.Replace("Attribute", "");
}
// Ищет базовый класс из нашего списка
public static string? GetHandlerBaseClassName(this ClassDeclarationSyntax classSyntax)
{
if (classSyntax.BaseList == null)
return null;
return classSyntax.BaseList.Types
.Select(t => t.Type.ToString())
.FirstOrDefault(name => HandlersNames.Contains(name));
}
// Достает namespace, в котором объявлен класс
public static string GetNamespace(this ClassDeclarationSyntax classDeclaration)
{
BaseNamespaceDeclarationSyntax? namespaceDeclaration = classDeclaration.FirstAncestorOrSelf<BaseNamespaceDeclarationSyntax>();
return namespaceDeclaration?.Name.ToString() ?? "Global";
}
}
@@ -0,0 +1,488 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable;
using System.Text;
namespace Telegrator.Analyzers;
[Generator(LanguageNames.CSharp)]
public class GeneratedKeyboardMarkupGenerator : IIncrementalGenerator
{
// Return types
private const string InlineReturnType = "InlineKeyboardMarkup";
private const string ReplyReturnType = "ReplyKeyboardMarkup";
// Attribute names
private const string CallbackDataAttribute = "CallbackButton";
private const string CallbackGameAttribute = "GameButton";
private const string CopyTextAttribute = "CopyTextButton";
private const string LoginRequestAttribute = "LoginRequestButton";
private const string PayRequestAttribute = "PayRequestButton";
private const string SwitchQueryAttribute = "SwitchQueryButton";
private const string QueryChosenAttribute = "QueryChosenButton";
private const string QueryCurrentAttribute = "QueryCurrentButton";
private const string UrlRedirectAttribute = "UrlRedirectButton";
private const string RequestChatAttribute = "RequestChatButton";
private const string RequestContactAttribute = "RequestContactButton";
private const string RequestLocationAttribute = "RequestLocationButton";
private const string RequestPoolAttribute = "RequestPoolButton";
private const string RequestUsersAttribute = "RequestUsersButton";
private const string WebAppAttribute = "WebApp";
// Markup lists
private static readonly string[] InlineAttributes = [CallbackDataAttribute, CallbackGameAttribute, CopyTextAttribute, LoginRequestAttribute, PayRequestAttribute, UrlRedirectAttribute, WebAppAttribute, SwitchQueryAttribute, QueryChosenAttribute, QueryCurrentAttribute];
private static readonly string[] ReplyAttributes = [RequestChatAttribute, RequestContactAttribute, RequestLocationAttribute, RequestPoolAttribute, RequestUsersAttribute, WebAppAttribute];
// Usings
private static readonly string[] DefaultUsings = ["Telegram.Bot.Types.ReplyMarkups"];
// Markup layouts
private static readonly Dictionary<string, MemberAccessExpressionSyntax> InlineKeyboardLayout = new Dictionary<string, MemberAccessExpressionSyntax>()
{
{ CallbackDataAttribute, AccessExpression("InlineKeyboardButton", "WithCallbackData") },
{ CallbackGameAttribute, AccessExpression("InlineKeyboardButton", "WithCallbackGame") },
{ CopyTextAttribute, AccessExpression("InlineKeyboardButton", "WithCopyText") },
{ LoginRequestAttribute, AccessExpression("InlineKeyboardButton", "WithLoginUrl") },
{ PayRequestAttribute, AccessExpression("InlineKeyboardButton", "WithPay") },
{ SwitchQueryAttribute, AccessExpression("InlineKeyboardButton", "WithSwitchInlineQuery") },
{ QueryChosenAttribute, AccessExpression("InlineKeyboardButton", "WithSwitchInlineQueryChosenChat") },
{ QueryCurrentAttribute, AccessExpression("InlineKeyboardButton", "WithSwitchInlineQueryCurrentChat") },
{ UrlRedirectAttribute, AccessExpression("InlineKeyboardButton", "WithUrl") },
{ WebAppAttribute, AccessExpression("InlineKeyboardButton", "WithWebApp") },
};
private static readonly Dictionary<string, MemberAccessExpressionSyntax> ReplyKeyboardLayout = new Dictionary<string, MemberAccessExpressionSyntax>()
{
{ RequestChatAttribute, AccessExpression("KeyboardButton", "WithRequestChat") },
{ RequestContactAttribute, AccessExpression("KeyboardButton", "WithRequestContact") },
{ RequestLocationAttribute, AccessExpression("KeyboardButton", "WithRequestLocation") },
{ RequestPoolAttribute, AccessExpression("KeyboardButton", "WithRequestPoll") },
{ RequestUsersAttribute, AccessExpression("KeyboardButton", "WithRequestUsers") },
{ WebAppAttribute, AccessExpression("KeyboardButton", "WithWebApp") }
};
// Markup map
private static readonly Dictionary<string, Dictionary<string, MemberAccessExpressionSyntax>> LayoutNames = new Dictionary<string, Dictionary<string, MemberAccessExpressionSyntax>>()
{
{ InlineReturnType, InlineKeyboardLayout },
{ ReplyReturnType, ReplyKeyboardLayout }
};
// Diagnostic descriptors
private static readonly DiagnosticDescriptor WrongReturnType = new DiagnosticDescriptor("TLG201", "Wrong return type", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor UnsupportedAttribute = new DiagnosticDescriptor("TLG202", "Unsupported or invalid attribute", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor NotPartialMethod = new DiagnosticDescriptor("TLG203", "Not a partial member", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor UseBodylessMethod = new DiagnosticDescriptor("TLG204", "Use bodyless method", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor UseParametrlessMethod = new DiagnosticDescriptor("TLG205", "Use parametrless method", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor UseGetOnlyProperty = new DiagnosticDescriptor("TLG206", "Use property with only get accessor", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor UseBodylessGetAccessor = new DiagnosticDescriptor("TLG207", "Use bodyless get accessor", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<MethodDeclarationSyntax>> methodsPipeline = context.SyntaxProvider.CreateSyntaxProvider(ProvideMethods, TransformMethods).Where(x => x != null).Collect();
IncrementalValueProvider<ImmutableArray<PropertyDeclarationSyntax>> propertiesPipeline = context.SyntaxProvider.CreateSyntaxProvider(ProvideProperties, TransformProperties).Where(x => x != null).Collect();
context.RegisterSourceOutput(methodsPipeline, ExecuteMethodsPipeline);
context.RegisterSourceOutput(propertiesPipeline, ExecutePropertiesPipeline);
}
private static bool ProvideMethods(SyntaxNode syntaxNode, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (syntaxNode is not MethodDeclarationSyntax method)
return false;
if (!HasGenAttributes(method))
return false;
return true;
}
private static bool ProvideProperties(SyntaxNode syntaxNode, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (syntaxNode is not PropertyDeclarationSyntax property)
return false;
if (!HasGenAttributes(property))
return false;
return true;
}
private static MethodDeclarationSyntax TransformMethods(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return (MethodDeclarationSyntax)context.Node;
}
private static PropertyDeclarationSyntax TransformProperties(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return (PropertyDeclarationSyntax)context.Node;
}
private static void ExecutePropertiesPipeline(SourceProductionContext context, ImmutableArray<PropertyDeclarationSyntax> properties)
{
List<GeneratedMarkupPropertyModel> models = new List<GeneratedMarkupPropertyModel>();
foreach (PropertyDeclarationSyntax prop in properties)
{
context.CancellationToken.ThrowIfCancellationRequested();
try
{
string returnType = prop.Type.ToString();
bool anyErrors = false;
Dictionary<string, MemberAccessExpressionSyntax> layout;
if (!LayoutNames.TryGetValue(returnType, out layout!))
{
context.ReportDiagnostic(Diagnostic.Create(WrongReturnType, prop.Type.GetLocation()));
anyErrors = true;
}
if (!prop.Modifiers.Any(SyntaxKind.PartialKeyword))
{
context.ReportDiagnostic(Diagnostic.Create(NotPartialMethod, prop.Identifier.GetLocation()));
anyErrors = true;
}
if (prop.Initializer != null)
{
context.ReportDiagnostic(Diagnostic.Create(UseGetOnlyProperty, prop.Initializer.GetLocation()));
anyErrors = true;
}
if (prop.ExpressionBody != null)
{
context.ReportDiagnostic(Diagnostic.Create(UseGetOnlyProperty, prop.ExpressionBody.GetLocation()));
anyErrors = true;
}
if (prop.AccessorList != null)
{
foreach (AccessorDeclarationSyntax accessor in prop.AccessorList.Accessors)
{
if (accessor.IsKind(SyntaxKind.SetAccessorDeclaration))
{
context.ReportDiagnostic(Diagnostic.Create(UseGetOnlyProperty, accessor.GetLocation()));
anyErrors = true;
continue;
}
if (accessor.Body != null)
{
context.ReportDiagnostic(Diagnostic.Create(UseBodylessGetAccessor, accessor.Body.GetLocation()));
anyErrors = true;
continue;
}
if (accessor.ExpressionBody != null)
{
context.ReportDiagnostic(Diagnostic.Create(UseBodylessGetAccessor, accessor.ExpressionBody.GetLocation()));
anyErrors = true;
continue;
}
}
}
if (anyErrors || layout == null)
continue;
SeparatedSyntaxList<CollectionElementSyntax> matrix = ParseAttributesMatrix(context, layout, prop);
PropertyDeclarationSyntax genProp = GeneratedPropertyDeclaration(prop, SyntaxFactory.CollectionExpression(matrix));
models.Add(new GeneratedMarkupPropertyModel(prop, genProp));
}
catch (Exception ex)
{
context.AddSource($"{prop.Identifier}_Error.g.cs", SourceText.From($"/* {ex} */", Encoding.UTF8));
}
}
if (models.Count == 0)
return;
CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit();
SyntaxList<UsingDirectiveSyntax> usingDirectives = SyntaxFactory.List(ParseUsings(DefaultUsings));
foreach (GeneratedMarkupPropertyModel model in models)
{
context.CancellationToken.ThrowIfCancellationRequested();
try
{
MemberDeclarationSyntax wrappedMember = WrapInParentDeclarations(model.OriginalProperty, new List<MemberDeclarationSyntax> { model.GeneratedProperty });
compilationUnit = compilationUnit.AddMembers(wrappedMember);
}
catch (Exception ex)
{
context.AddSource($"{model.OriginalProperty.Identifier}_GenError.g.cs", SourceText.From($"/* {ex} */", Encoding.UTF8));
}
}
compilationUnit = compilationUnit.WithUsings(usingDirectives).NormalizeWhitespace();
context.AddSource("GeneratedKeyboards.Properties.g.cs", SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
}
private static void ExecuteMethodsPipeline(SourceProductionContext context, ImmutableArray<MethodDeclarationSyntax> methods)
{
List<GeneratedMarkupMethodModel> models = new List<GeneratedMarkupMethodModel>();
foreach (MethodDeclarationSyntax method in methods)
{
context.CancellationToken.ThrowIfCancellationRequested();
try
{
string methodName = method.Identifier.Text;
string returnType = method.ReturnType.ToString();
bool anyErrors = false;
Dictionary<string, MemberAccessExpressionSyntax> layout;
if (!LayoutNames.TryGetValue(returnType, out layout!))
{
context.ReportDiagnostic(Diagnostic.Create(WrongReturnType, method.ReturnType.GetLocation()));
anyErrors = true;
}
if (!method.Modifiers.Any(SyntaxKind.PartialKeyword))
{
context.ReportDiagnostic(Diagnostic.Create(NotPartialMethod, method.Identifier.GetLocation()));
anyErrors = true;
}
if (method.ParameterList.Parameters.Count > 0)
{
context.ReportDiagnostic(Diagnostic.Create(UseParametrlessMethod, method.ParameterList.GetLocation()));
anyErrors = true;
}
if (method.ExpressionBody != null)
{
context.ReportDiagnostic(Diagnostic.Create(UseBodylessMethod, method.ExpressionBody.GetLocation()));
anyErrors = true;
}
if (method.Body != null)
{
context.ReportDiagnostic(Diagnostic.Create(UseBodylessMethod, method.Body.GetLocation()));
anyErrors = true;
}
if (anyErrors || layout == null)
continue;
SeparatedSyntaxList<CollectionElementSyntax> matrix = ParseAttributesMatrix(context, layout, method);
FieldDeclarationSyntax genField = GeneratedFieldDeclaration(methodName, method.ReturnType, SyntaxFactory.CollectionExpression(matrix));
MethodDeclarationSyntax genMethod = GeneratedMethodDeclaration(methodName, method.Modifiers, method.ReturnType, genField);
models.Add(new GeneratedMarkupMethodModel(method, genField, genMethod));
}
catch (Exception ex)
{
context.AddSource($"{method.Identifier}_Error.g.cs", SourceText.From($"/* {ex} */", Encoding.UTF8));
}
}
if (models.Count == 0)
return;
CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit();
SyntaxList<UsingDirectiveSyntax> usingDirectives = SyntaxFactory.List(ParseUsings(DefaultUsings));
foreach (GeneratedMarkupMethodModel model in models)
{
context.CancellationToken.ThrowIfCancellationRequested();
try
{
MemberDeclarationSyntax wrappedMembers = WrapInParentDeclarations(model.OriginalMethod, new List<MemberDeclarationSyntax> { model.GeneratedField, model.GeneratedMethod });
compilationUnit = compilationUnit.AddMembers(wrappedMembers);
}
catch (Exception ex)
{
context.AddSource($"{model.OriginalMethod.Identifier}_GenError.g.cs", SourceText.From($"/* {ex} */", Encoding.UTF8));
}
}
compilationUnit = compilationUnit.WithUsings(usingDirectives).NormalizeWhitespace();
context.AddSource("GeneratedKeyboards.Methods.g.cs", SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
}
private static SeparatedSyntaxList<CollectionElementSyntax> ParseAttributesMatrix(SourceProductionContext context, Dictionary<string, MemberAccessExpressionSyntax> layout, MemberDeclarationSyntax member)
{
SeparatedSyntaxList<CollectionElementSyntax> vertical = new SeparatedSyntaxList<CollectionElementSyntax>();
foreach (AttributeListSyntax attributeList in member.AttributeLists)
{
context.CancellationToken.ThrowIfCancellationRequested();
SeparatedSyntaxList<CollectionElementSyntax> horizontal = new SeparatedSyntaxList<CollectionElementSyntax>();
foreach (AttributeSyntax attribute in attributeList.Attributes)
{
context.CancellationToken.ThrowIfCancellationRequested();
MemberAccessExpressionSyntax accessSyntax;
if (!layout.TryGetValue(attribute.Name.ToString(), out accessSyntax!))
{
context.ReportDiagnostic(Diagnostic.Create(UnsupportedAttribute, attribute.Name.GetLocation()));
continue;
}
InvocationExpressionSyntax expression = SyntaxFactory.InvocationExpression(accessSyntax, ConvertArguments(attribute.ArgumentList));
horizontal = horizontal.Add(SyntaxFactory.ExpressionElement(expression));
}
ExpressionElementSyntax element = SyntaxFactory.ExpressionElement(SyntaxFactory.CollectionExpression(horizontal));
vertical = vertical.Add(element);
}
return vertical;
}
private static PropertyDeclarationSyntax GeneratedPropertyDeclaration(PropertyDeclarationSyntax property, CollectionExpressionSyntax collection)
{
return SyntaxFactory.PropertyDeclaration(property.Type, property.Identifier)
.WithExpressionBody(SyntaxFactory.ArrowExpressionClause(collection))
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
}
private static MethodDeclarationSyntax GeneratedMethodDeclaration(string identifier, SyntaxTokenList modifiers, TypeSyntax returnType, FieldDeclarationSyntax field)
{
VariableDeclaratorSyntax targetVariable = field.Declaration.Variables.First();
return SyntaxFactory.MethodDeclaration(returnType, identifier)
.WithModifiers(modifiers)
.WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.IdentifierName(targetVariable.Identifier)))
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
}
private static FieldDeclarationSyntax GeneratedFieldDeclaration(string identifier, TypeSyntax returnType, CollectionExpressionSyntax collection)
{
ArgumentSyntax argument = SyntaxFactory.Argument(collection);
ArgumentListSyntax arguments = SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(argument));
ObjectCreationExpressionSyntax objectCreation = SyntaxFactory.ObjectCreationExpression(returnType, arguments, null);
VariableDeclaratorSyntax declarator = SyntaxFactory.VariableDeclarator(identifier + "_generatedMarkup")
.WithInitializer(SyntaxFactory.EqualsValueClause(objectCreation));
SyntaxTokenList fieldModifiers = SyntaxFactory.TokenList(
SyntaxFactory.Token(SyntaxKind.PrivateKeyword),
SyntaxFactory.Token(SyntaxKind.StaticKeyword),
SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword));
return SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration(returnType).AddVariables(declarator))
.WithModifiers(fieldModifiers);
}
private static ArgumentListSyntax ConvertArguments(AttributeArgumentListSyntax? attributeArgs)
{
if (attributeArgs == null)
return SyntaxFactory.ArgumentList();
IEnumerable<ArgumentSyntax> arguments = attributeArgs.Arguments.Select(CastArgument);
return SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments));
}
private static ArgumentSyntax CastArgument(AttributeArgumentSyntax argument)
{
if (argument.NameColon != null)
{
return SyntaxFactory.Argument(argument.Expression).WithNameColon(argument.NameColon);
}
return SyntaxFactory.Argument(argument.Expression);
}
private static MemberDeclarationSyntax WrapInParentDeclarations(MemberDeclarationSyntax originalMember, List<MemberDeclarationSyntax> generatedMembers)
{
SyntaxNode? parentNode = originalMember.Parent;
if (parentNode is not ClassDeclarationSyntax)
{
throw new InvalidOperationException("Generated member must be contained within a class.");
}
MemberDeclarationSyntax currentDeclaration = SyntaxFactory.ClassDeclaration(((ClassDeclarationSyntax)parentNode).Identifier)
.WithMembers(SyntaxFactory.List(generatedMembers))
.WithModifiers(((ClassDeclarationSyntax)parentNode).Modifiers);
if (!currentDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
{
currentDeclaration = ((ClassDeclarationSyntax)currentDeclaration).AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword));
}
parentNode = parentNode.Parent;
while (parentNode is TypeDeclarationSyntax typeDeclaration)
{
ClassDeclarationSyntax wrappingClass = SyntaxFactory.ClassDeclaration(typeDeclaration.Identifier)
.WithMembers(SyntaxFactory.SingletonList(currentDeclaration))
.WithModifiers(typeDeclaration.Modifiers);
if (!wrappingClass.Modifiers.Any(SyntaxKind.PartialKeyword))
{
wrappingClass = wrappingClass.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword));
}
currentDeclaration = wrappingClass;
parentNode = parentNode.Parent;
}
if (parentNode is BaseNamespaceDeclarationSyntax namespaceDeclaration)
{
currentDeclaration = SyntaxFactory.NamespaceDeclaration(namespaceDeclaration.Name)
.WithMembers(SyntaxFactory.SingletonList(currentDeclaration));
}
return currentDeclaration;
}
private static IEnumerable<UsingDirectiveSyntax> ParseUsings(params string[] names)
{
return names.Select(name => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(name)));
}
private static bool HasGenAttributes(MemberDeclarationSyntax member)
{
IEnumerable<string> memberAttributes = member.AttributeLists
.SelectMany(x => x.Attributes)
.Select(x => x.Name.ToString());
IEnumerable<string> targetAttributes = InlineAttributes.Concat(ReplyAttributes);
return memberAttributes.Intersect(targetAttributes).Any();
}
private static MemberAccessExpressionSyntax AccessExpression(string className, string methodName)
{
return SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName(className),
SyntaxFactory.IdentifierName(methodName));
}
private class GeneratedMarkupMethodModel
{
public MethodDeclarationSyntax OriginalMethod { get; }
public FieldDeclarationSyntax GeneratedField { get; }
public MethodDeclarationSyntax GeneratedMethod { get; }
public GeneratedMarkupMethodModel(MethodDeclarationSyntax originalMethod, FieldDeclarationSyntax generatedField, MethodDeclarationSyntax generatedMethod)
{
OriginalMethod = originalMethod;
GeneratedField = generatedField;
GeneratedMethod = generatedMethod;
}
}
private class GeneratedMarkupPropertyModel
{
public PropertyDeclarationSyntax OriginalProperty { get; }
public PropertyDeclarationSyntax GeneratedProperty { get; }
public GeneratedMarkupPropertyModel(PropertyDeclarationSyntax originalProperty, PropertyDeclarationSyntax generatedProperty)
{
OriginalProperty = originalProperty;
GeneratedProperty = generatedProperty;
}
}
}
@@ -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", "IDE0090")]
@@ -0,0 +1,64 @@
namespace Telegrator.Analyzers.RoslynExtensions
{
public static class CollectionsExtensions
{
public static IEnumerable<TSource> Combine<TSource>(params IEnumerable<TSource>[] collections)
=> collections.SelectMany(x => x);
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;
}
public static void UnionAdd<TSource>(this ICollection<TSource> collection, IEnumerable<TSource> target)
{
foreach (TSource item in target)
{
if (!collection.Contains(item))
collection.Add(item);
}
}
public static void UnionAdd<TSource>(this SortedList<TSource, TSource> collection, IEnumerable<TSource> target)
{
foreach (TSource item in target)
{
if (!collection.Values.Contains(item))
collection.Add(item, item);
}
}
public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
int index = 0;
foreach (T item in source)
{
if (predicate.Invoke(item))
return index;
index++;
}
return -1;
}
public static IEnumerable<T> Repeat<T>(this T item, int times)
=> Enumerable.Range(0, times).Select(_ => item);
}
}
@@ -0,0 +1,16 @@
using Microsoft.CodeAnalysis;
namespace Telegrator.Analyzers.RoslynExtensions
{
public static class DiagnosticsHelper
{
public static Diagnostic Create(this DiagnosticDescriptor descriptor, Location? location, params object[] messageArgs)
=> Diagnostic.Create(descriptor, location, messageArgs);
public static void Report(this Diagnostic diagnostic, SourceProductionContext context)
=> context.ReportDiagnostic(diagnostic);
public static void Report(this DiagnosticDescriptor descriptor, SourceProductionContext context, Location? location, params object[] messageArgs)
=> descriptor.Create(location, messageArgs).Report(context);
}
}
@@ -0,0 +1,7 @@
namespace Telegrator.Analyzers.RoslynExtensions;
public class TargteterNotFoundException() : Exception() { }
public class BaseClassTypeNotFoundException() : Exception() { }
public class AncestorNotFoundException : Exception { }
@@ -0,0 +1,38 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Telegrator.Analyzers.RoslynExtensions
{
public static class MemberDeclarationSyntaxExtensions
{
private static SyntaxTrivia TabulationTrivia => SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, "\t");
private static SyntaxTrivia WhitespaceTrivia => SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " ");
private static SyntaxTrivia NewLineTrivia => SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, "\n");
public static SyntaxTokenList Decorate(this SyntaxTokenList tokens)
=> new SyntaxTokenList(tokens.Select(token => token.WithoutTrivia().WithTrailingTrivia(WhitespaceTrivia)).ToArray());
public static BlockSyntax DecorateBlock(this BlockSyntax block, int times) => block
.WithStatements([.. block.Statements.Select(statement => statement.DecorateStatememnt(times + 1))])
.WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia))
.WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken).WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia));
public static T DecorateStatememnt<T>(this T statememnt, int times) where T : StatementSyntax => statememnt
.WithoutTrivia().WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia);
public static T DecorateMember<T>(this T typeDeclaration, int times) where T : MemberDeclarationSyntax => typeDeclaration
.WithoutTrivia().WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia);
public static NamespaceDeclarationSyntax Decorate(this NamespaceDeclarationSyntax namespaceDeclaration) => namespaceDeclaration
.WithName(namespaceDeclaration.Name.WithoutTrivia().WithLeadingTrivia(WhitespaceTrivia))
.WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithLeadingTrivia(NewLineTrivia).WithTrailingTrivia(NewLineTrivia))
.WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken));
public static T DecorateType<T>(this T typeDeclaration, int times = 1) where T : TypeDeclarationSyntax => (T)typeDeclaration
.WithoutTrivia().WithLeadingTrivia(TabulationTrivia.Repeat(times))
.WithIdentifier(typeDeclaration.Identifier.WithoutTrivia().WithLeadingTrivia(WhitespaceTrivia).WithTrailingTrivia(NewLineTrivia))
.WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia))
.WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken).WithLeadingTrivia(TabulationTrivia.Repeat(times)).WithTrailingTrivia(NewLineTrivia));
}
}
@@ -0,0 +1,10 @@
using System.Text;
namespace Telegrator.Analyzers.RoslynExtensions
{
public static class StringBuilderExtensions
{
public static StringBuilder AppendTabs(this StringBuilder builder, int count)
=> builder.Append(new string('\t', count));
}
}
@@ -0,0 +1,21 @@
namespace Telegrator.Analyzers.RoslynExtensions
{
public static class StringExtensions
{
public static string FirstLetterToUpper(this string target)
{
char[] chars = target.ToCharArray();
int index = chars.IndexOf(char.IsLetter);
chars[index] = char.ToUpper(chars[index]);
return new string(chars);
}
public static string FirstLetterToLower(this string target)
{
char[] chars = target.ToCharArray();
int index = chars.IndexOf(char.IsLetter);
chars[index] = char.ToLower(chars[index]);
return new string(chars);
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.CodeAnalysis;
namespace Telegrator.Analyzers.RoslynExtensions;
public static class SymbolsExtensions
{
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);
}
}
@@ -0,0 +1,74 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Telegrator.Analyzers.RoslynExtensions
{
public static class SyntaxNodesExtensions
{
public static T FindAncestor<T>(this SyntaxNode node) where T : SyntaxNode
{
if (node.Parent == null)
throw new AncestorNotFoundException();
if (node.Parent is T found)
return found;
return node.Parent.FindAncestor<T>();
}
public static bool TryFindAncestor<T>(this SyntaxNode node, out T syntax) where T : SyntaxNode
{
if (node.Parent == null)
{
syntax = null!;
return false;
}
if (node.Parent is T found)
{
syntax = found;
return true;
}
return node.Parent.TryFindAncestor(out syntax);
}
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 int CountParentTree(this SyntaxNode node)
{
int count = 0;
SyntaxNode inspectNode = node;
while (inspectNode.Parent != null)
{
inspectNode = inspectNode.Parent;
count++;
}
return count;
}
public static SeparatedSyntaxList<TNode> ToSeparatedSyntaxList<TNode>(this IEnumerable<TNode> elements) where TNode : SyntaxNode
=> new SeparatedSyntaxList<TNode>().AddRange(elements);
public static SyntaxList<TNode> ToSyntaxList<TNode>(this IEnumerable<TNode> elements) where TNode : SyntaxNode
=> new SyntaxList<TNode>().AddRange(elements);
}
}
@@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;
namespace Telegrator.Analyzers.RoslynExtensions
{
public static class SyntaxTokenExtensions
{
public static bool HasModifiers(this SyntaxTokenList modifiers, params string[] expected)
{
return modifiers.Count(mod => expected.Contains(mod.ToString())) == expected.Length;
}
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</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" />
<PackageReference Include="PolySharp" Version="1.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Telegrator.Hosting.Components;
namespace Telegrator.Hosting.Web.Components
{
/// <summary>
/// Interface for Telegram bot hosts with Webhook update receiving.
/// Combines wbe application capabilities with reactive Telegram bot functionality.
/// </summary>
public interface ITelegramBotWebHost : ITelegramBotHost, IEndpointRouteBuilder, IApplicationBuilder, IAsyncDisposable
{
}
}
@@ -0,0 +1,10 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0290")]
[assembly: SuppressMessage("Style", "IDE0090")]
[assembly: SuppressMessage("Usage", "CA2254")]
@@ -0,0 +1,96 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using System.Text.Json;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegrator.MadiatorCore;
namespace Telegrator.Hosting.Web.Polling
{
/// <summary>
/// Service for receiving updates for Hosted telegram bots via Webhooks
/// </summary>
public class HostedUpdateWebhooker : IHostedService
{
private const string SecretTokenHeader = "X-Telegram-Bot-Api-Secret-Token";
private readonly IEndpointRouteBuilder _botHost;
private readonly ITelegramBotClient _botClient;
private readonly IUpdateRouter _updateRouter;
private readonly TelegratorWebOptions _options;
/// <summary>
/// Initiallizes new instance of <see cref="HostedUpdateWebhooker"/>
/// </summary>
/// <param name="botHost"></param>
/// <param name="botClient"></param>
/// <param name="updateRouter"></param>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
public HostedUpdateWebhooker(IEndpointRouteBuilder botHost, ITelegramBotClient botClient, IUpdateRouter updateRouter, IOptions<TelegratorWebOptions> options)
{
if (string.IsNullOrEmpty(options.Value.WebhookUri))
throw new ArgumentNullException(nameof(options), "Option \"WebhookUrl\" must be set to subscribe for update recieving");
_botHost = botHost;
_botClient = botClient;
_updateRouter = updateRouter;
_options = options.Value;
}
/// <inheritdoc/>
public Task StartAsync(CancellationToken cancellationToken)
{
StartInternal(cancellationToken);
return Task.CompletedTask;
}
private async void StartInternal(CancellationToken cancellationToken)
{
string pattern = new UriBuilder(_options.WebhookUri).Path;
_botHost.MapPost(pattern, (Delegate)ReceiveUpdate);
await _botClient.SetWebhook(
url: _options.WebhookUri,
maxConnections: _options.MaxConnections,
allowedUpdates: _updateRouter.HandlersProvider.AllowedTypes,
dropPendingUpdates: _options.DropPendingUpdates,
secretToken: _options.SecretToken,
cancellationToken: cancellationToken);
}
/// <inheritdoc/>
public Task StopAsync(CancellationToken cancellationToken)
{
_botClient.DeleteWebhook(_options.DropPendingUpdates, cancellationToken);
return Task.CompletedTask;
}
private async Task<IResult> ReceiveUpdate(HttpContext ctx)
{
if (_options.SecretToken != null)
{
if (!ctx.Request.Headers.TryGetValue(SecretTokenHeader, out StringValues strings))
return Results.BadRequest();
string? secret = strings.SingleOrDefault();
if (secret == null)
return Results.BadRequest();
if (_options.SecretToken != secret)
return Results.StatusCode(401);
}
Update? update = await JsonSerializer.DeserializeAsync<Update>(ctx.Request.Body, JsonBotAPI.Options, ctx.RequestAborted);
if (update is not { Id: > 0 })
return Results.BadRequest();
await _updateRouter.HandleUpdateAsync(_botClient, update, ctx.RequestAborted);
return Results.Ok();
}
}
}
+87
View File
@@ -0,0 +1,87 @@
# Telegrator.Hosting.Web
**Telegrator.Hosting.Web** is an extension for the Telegrator framework that enables seamless integration with ASP.NET Core and webhook-based Telegram bots. It is designed for scalable, production-ready web applications.
---
## Features
- ASP.NET Core integration for webhook-based bots
- Automatic handler discovery and registration
- Strongly-typed configuration via `appsettings.json` and environment variables
- Dependency injection and middleware support
- Graceful startup/shutdown and lifecycle management
- Advanced error handling and logging
- Supports all Telegrator handler/filter/state features
---
## Requirements
- .NET 8.0 or later
- ASP.NET Core
---
## Installation
```shell
dotnet add package Telegrator.Hosting.Web
```
---
## Quick Start Example
**Program.cs (ASP.NET Core):**
```csharp
using Telegrator.Hosting;
using Telegrator.Hosting.Web;
// Creating builder
TelegramBotWebHostBuilder builder = TelegramBotWebHost.CreateBuilder(new TelegramBotWebOptions()
{
Args = args,
ExceptIntersectingCommandAliases = true
});
// Register handlers
builder.Handlers.CollectHandlersAssemblyWide();
// Register your services
builder.Services.AddSingleton<IMyService, MyService>();
// Building and running application
TelegramBotWebHost telegramBot = builder.Build();
telegramBot.SetBotCommands();
telegramBot.Run();
```
---
## Configuration (appsettings.json)
```json
{
"TelegramBotClientOptions": {
"Token": "YOUR_BOT_TOKEN"
}
"TelegratorWebOptions": {
"WebhookUri" = "https://you-public-host.ru/bot",
"DropPendingUpdates": true
}
}
```
- `TelegramBotClientOptions`: Bot token and client settings
---
## Documentation
- [Telegrator Main Docs](https://github.com/Rikitav/Telegrator)
- [Getting Started Guide](https://github.com/Rikitav/Telegrator/wiki/Getting-started)
- [Annotation Overview](https://github.com/Rikitav/Telegrator/wiki/Annotation-overview)
---
## License
GPLv3
@@ -0,0 +1,177 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Telegrator.Hosting.Components;
using Telegrator.Hosting.Web.Components;
using Telegrator.MadiatorCore;
namespace Telegrator.Hosting.Web
{
/// <summary>
/// Represents a web hosted telegram bot
/// </summary>
public class TelegramBotWebHost : ITelegramBotWebHost
{
private readonly WebApplication _innerApp;
private readonly IUpdateRouter _updateRouter;
private readonly ILogger<TelegramBotWebHost> _logger;
private bool _disposed;
/// <inheritdoc/>
public IServiceProvider Services => _innerApp.Services;
/// <inheritdoc/>
public IUpdateRouter UpdateRouter => _updateRouter;
/// <inheritdoc/>
public ICollection<EndpointDataSource> DataSources => ((IEndpointRouteBuilder)_innerApp).DataSources;
/// <summary>
/// Allows consumers to be notified of application lifetime events.
/// </summary>
public IHostApplicationLifetime Lifetime => _innerApp.Lifetime;
/// <summary>
/// This application's logger
/// </summary>
public ILogger<TelegramBotWebHost> Logger => _logger;
// Private interface fields
IServiceProvider IEndpointRouteBuilder.ServiceProvider => Services;
IServiceProvider IApplicationBuilder.ApplicationServices { get => Services; set => throw new NotImplementedException(); }
IFeatureCollection IApplicationBuilder.ServerFeatures => ((IApplicationBuilder)_innerApp).ServerFeatures;
IDictionary<string, object?> IApplicationBuilder.Properties => ((IApplicationBuilder)_innerApp).Properties;
/// <summary>
/// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class.
/// </summary>
/// <param name="webApplicationBuilder">The proxied instance of host builder.</param>
/// <param name="handlers"></param>
public TelegramBotWebHost(WebApplicationBuilder webApplicationBuilder, IHandlersCollection handlers)
{
// Registering this host in services for easy access
RegisterHostServices(webApplicationBuilder.Services, handlers);
// Building proxy application
_innerApp = webApplicationBuilder.Build();
_innerApp.UseTelegratorWeb();
// Reruesting services for this host
_updateRouter = Services.GetRequiredService<IUpdateRouter>();
_logger = Services.GetRequiredService<ILogger<TelegramBotWebHost>>();
}
/// <summary>
/// Creates new <see cref="TelegramBotHostBuilder"/> with default services and webhook update receiving scheme
/// </summary>
/// <returns></returns>
public static TelegramBotWebHostBuilder CreateBuilder(TelegramBotWebOptions settings)
{
ArgumentNullException.ThrowIfNull(settings, nameof(settings));
WebApplicationBuilder innerApp = WebApplication.CreateBuilder(settings.ToWebApplicationOptions());
TelegramBotWebHostBuilder builder = new TelegramBotWebHostBuilder(innerApp, settings);
builder.Services.AddTelegramBotHostDefaults();
builder.Services.AddTelegramWebhook();
return builder;
}
/// <summary>
/// Creates new SLIM <see cref="TelegramBotHostBuilder"/> with default services and webhook update receiving scheme
/// </summary>
/// <returns></returns>
public static TelegramBotWebHostBuilder CreateSlimBuilder(TelegramBotWebOptions settings)
{
ArgumentNullException.ThrowIfNull(settings, nameof(settings));
WebApplicationBuilder innerApp = WebApplication.CreateSlimBuilder(settings.ToWebApplicationOptions());
TelegramBotWebHostBuilder builder = new TelegramBotWebHostBuilder(innerApp, settings);
builder.Services.AddTelegramBotHostDefaults();
builder.Services.AddTelegramWebhook();
return builder;
}
/// <summary>
/// Creates new EMPTY <see cref="TelegramBotHostBuilder"/> WITHOUT any services or update receiving schemes
/// </summary>
/// <returns></returns>
public static TelegramBotWebHostBuilder CreateEmptyBuilder(TelegramBotWebOptions settings)
{
ArgumentNullException.ThrowIfNull(settings, nameof(settings));
WebApplicationBuilder innerApp = WebApplication.CreateEmptyBuilder(settings.ToWebApplicationOptions());
return new TelegramBotWebHostBuilder(innerApp, settings);
}
/// <inheritdoc/>
public async Task StartAsync(CancellationToken cancellationToken = default)
{
await _innerApp.StartAsync(cancellationToken);
}
/// <inheritdoc/>
public async Task StopAsync(CancellationToken cancellationToken = default)
{
await _innerApp.StopAsync(cancellationToken);
}
/// <inheritdoc/>
public IApplicationBuilder CreateApplicationBuilder()
=> ((IEndpointRouteBuilder)_innerApp).CreateApplicationBuilder();
/// <inheritdoc/>
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
=> _innerApp.Use(middleware);
/// <inheritdoc/>
public IApplicationBuilder New()
=> ((IApplicationBuilder)_innerApp).New();
/// <inheritdoc/>
public RequestDelegate Build()
=> ((IApplicationBuilder)_innerApp).Build();
/// <summary>
/// Disposes the host.
/// </summary>
public async ValueTask DisposeAsync()
{
if (_disposed)
return;
await _innerApp.DisposeAsync();
GC.SuppressFinalize(this);
_disposed = true;
}
/// <summary>
/// Disposes the host.
/// </summary>
public void Dispose()
{
if (_disposed)
return;
// Sorry for this, i really dont know how to handle such cases
ValueTask disposeTask = _innerApp.DisposeAsync();
while (!disposeTask.IsCompleted)
Thread.Sleep(100);
GC.SuppressFinalize(this);
_disposed = true;
}
private void RegisterHostServices(IServiceCollection services, IHandlersCollection handlers)
{
//service.RemoveAll<IHost>();
//service.AddSingleton<IHost>(this);
services.AddSingleton<ITelegramBotHost>(this);
services.AddSingleton<ITelegramBotWebHost>(this);
services.AddSingleton<ITelegratorBot>(this);
}
}
}
@@ -0,0 +1,108 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegram.Bot;
using Telegrator.Hosting.Components;
using Telegrator.Hosting.Configuration;
using Telegrator.Hosting.Providers;
using Telegrator.Hosting.Providers.Components;
using Telegrator.MadiatorCore;
#pragma warning disable IDE0001
namespace Telegrator.Hosting.Web
{
/// <summary>
/// Represents a web hosted telegram bots and services builder that helps manage configuration, logging, lifetime, and more.
/// </summary>
public class TelegramBotWebHostBuilder : ITelegramBotHostBuilder
{
private readonly WebApplicationBuilder _innerBuilder;
private readonly TelegramBotWebOptions _settings;
private readonly IHandlersCollection _handlers;
/// <inheritdoc/>
public IHandlersCollection Handlers => _handlers;
/// <inheritdoc/>
public IConfigurationManager Configuration => _innerBuilder.Configuration;
/// <inheritdoc/>
public ILoggingBuilder Logging => _innerBuilder.Logging;
/// <inheritdoc/>
public IServiceCollection Services => _innerBuilder.Services;
/// <inheritdoc/>
public IHostEnvironment Environment => _innerBuilder.Environment;
/// <summary>
/// Initializes a new instance of the <see cref="TelegramBotWebHostBuilder"/> class.
/// </summary>
/// <param name="webApplicationBuilder"></param>
/// <param name="settings"></param>
public TelegramBotWebHostBuilder(WebApplicationBuilder webApplicationBuilder, TelegramBotWebOptions settings)
{
_innerBuilder = webApplicationBuilder ?? throw new ArgumentNullException(nameof(webApplicationBuilder));
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_handlers = new HostHandlersCollection(Services, _settings);
}
/// <summary>
/// Initializes a new instance of the <see cref="TelegramBotWebHostBuilder"/> class.
/// </summary>
/// <param name="webApplicationBuilder"></param>
/// <param name="handlers"></param>
/// <param name="settings"></param>
public TelegramBotWebHostBuilder(WebApplicationBuilder webApplicationBuilder, TelegramBotWebOptions settings, IHandlersCollection handlers)
{
_innerBuilder = webApplicationBuilder ?? throw new ArgumentNullException(nameof(webApplicationBuilder));
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_handlers = handlers ?? throw new ArgumentNullException(nameof(settings));
_innerBuilder.AddTelegratorWeb(settings);
}
/// <summary>
/// Builds the host.
/// </summary>
/// <returns></returns>
public TelegramBotWebHost Build()
{
if (_handlers is IHostHandlersCollection hostHandlers)
{
foreach (PreBuildingRoutine preBuildRoutine in hostHandlers.PreBuilderRoutines)
{
try
{
preBuildRoutine.Invoke(this);
}
catch (NotImplementedException)
{
_ = 0xBAD + 0xC0DE;
}
}
}
if (!_settings.DisableAutoConfigure)
{
Services.Configure<TelegratorWebOptions>(Configuration.GetSection(nameof(TelegratorWebOptions)));
Services.Configure<TelegramBotClientOptions>(Configuration.GetSection(nameof(TelegramBotClientOptions)), new TelegramBotClientOptionsProxy());
}
else
{
if (null == Services.SingleOrDefault(srvc => srvc.ImplementationType == typeof(IOptions<TelegratorWebOptions>)))
throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegratorWebOptions' wasn't registered. This configuration is runtime required!");
if (null == Services.SingleOrDefault(srvc => srvc.ImplementationType == typeof(IOptions<TelegramBotClientOptions>)))
throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegramBotClientOptions' wasn't registered. This configuration is runtime required!");
}
Services.AddSingleton<IConfigurationManager>(Configuration);
Services.AddSingleton<IOptions<TelegratorOptions>>(Options.Create(_settings));
return new TelegramBotWebHost(_innerBuilder, _handlers);
}
}
}
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Options;
namespace Telegrator.Hosting.Web
{
/// <summary>
/// Options for configuring the behavior for TelegramBotWebHost.
/// </summary>
public class TelegramBotWebOptions : TelegratorOptions
{
/// <summary>
/// Disables automatic configuration for all of required <see cref="IOptions{TOptions}"/> instances
/// </summary>
public bool DisableAutoConfigure { get; set; }
/// <inheritdoc cref="WebApplicationOptions.Args"/>
public string[]? Args { get; init; }
/// <inheritdoc cref="WebApplicationOptions.EnvironmentName"/>
public string? EnvironmentName { get; init; }
/// <inheritdoc cref="WebApplicationOptions.ApplicationName"/>
public string? ApplicationName { get; init; }
/// <inheritdoc cref="WebApplicationOptions.ContentRootPath"/>
public string? ContentRootPath { get; init; }
/// <inheritdoc cref="WebApplicationOptions.WebRootPath"/>
public string? WebRootPath { get; init; }
internal WebApplicationOptions ToWebApplicationOptions() => new WebApplicationOptions()
{
ApplicationName = ApplicationName,
Args = Args,
ContentRootPath = ContentRootPath,
EnvironmentName = EnvironmentName,
WebRootPath = WebRootPath
};
}
}
@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<BaseOutputPath>..\..\bin</BaseOutputPath>
<DocumentationFile>..\..\docs\$(AssemblyName).xml</DocumentationFile>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<EnableNETAnalyzers>True</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<Title>Telegrator.Hosting.Web</Title>
<Version>1.16.0</Version>
<Authors>Rikitav Tim4ik</Authors>
<Company>Rikitav Tim4ik</Company>
<RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl>
<PackageTags>telegram;bot;mediator;attributes;aspect;hosting;host;framework;easy;simple;handlers</PackageTags>
<PackageIcon>telegrator_nuget.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="True" PackagePath="\" />
<None Include="..\README.md" Pack="True" PackagePath="\" />
<None Include="..\resources\telegrator_nuget.png" Pack="True" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Telegrator.Hosting\Telegrator.Hosting.csproj" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Telegram.Bot.AspNetCore" Version="22.5.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,35 @@
using System.Diagnostics.CodeAnalysis;
namespace Telegrator.Hosting.Web
{
/// <summary>
/// Configuration options for Telegram bot behavior and execution settings.
/// Controls various aspects of bot operation including concurrency, routing, webhook receiving, and execution policies.
/// </summary>
public class TelegratorWebOptions
{
/// <summary>
/// Gets or sets HTTPS URL to send updates to. Use an empty string to remove webhook integration
/// </summary>
[StringSyntax(StringSyntaxAttribute.Uri)]
public required string WebhookUri { get; set; }
/// <summary>
/// A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 1-256 characters.
/// Only characters A-Z, a-z, 0-9, _ and - are allowed.
/// The header is useful to ensure that the request comes from a webhook set by you.
/// </summary>
public string? SecretToken { get; set; }
/// <summary>
/// The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to 40.
/// Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput.
/// </summary>
public int MaxConnections { get; set; } = 40;
/// <summary>
/// Pass true to drop all pending updates
/// </summary>
public bool DropPendingUpdates { get; set; }
}
}
@@ -0,0 +1,117 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using Telegram.Bot;
using Telegrator.Configuration;
using Telegrator.Hosting.Providers;
using Telegrator.Hosting.Providers.Components;
using Telegrator.Hosting.Web.Components;
using Telegrator.Hosting.Web.Polling;
using Telegrator.MadiatorCore;
namespace Telegrator.Hosting.Web
{
/// <summary>
/// Contains extensions for <see cref="IServiceCollection"/>
/// Provides method to configure <see cref="ITelegramBotWebHost"/>
/// </summary>
public static class ServicesCollectionExtensions
{
/// <summary>
/// Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers.
/// </summary>
public static WebApplicationBuilder AddTelegratorWeb(this WebApplicationBuilder builder, TelegramBotWebOptions settings, IHandlersCollection? handlers = null)
{
if (settings is null)
throw new ArgumentNullException(nameof(settings));
IServiceCollection services = builder.Services;
ConfigurationManager configuration = builder.Configuration;
handlers ??= new HostHandlersCollection(services, settings);
if (handlers is IHostHandlersCollection hostHandlers)
{
foreach (PreBuildingRoutine preBuildRoutine in hostHandlers.PreBuilderRoutines)
{
try
{
// TODO: fix
//preBuildRoutine.Invoke(builder);
Debug.WriteLine("Pre-Building routine was not executed");
}
catch (NotImplementedException)
{
_ = 0xBAD + 0xC0DE;
}
}
}
if (!settings.DisableAutoConfigure)
{
services.Configure<TelegratorWebOptions>(configuration.GetSection(nameof(TelegratorWebOptions)));
}
else
{
if (!services.Any(srvc => srvc.ImplementationType == typeof(IOptions<TelegratorWebOptions>)))
throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegratorWebOptions' wasn't registered. This configuration is runtime required!");
if (!services.Any(srvc => srvc.ImplementationType == typeof(IOptions<TelegramBotClientOptions>)))
throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegramBotClientOptions' wasn't registered. This configuration is runtime required!");
}
IOptions<TelegramBotWebOptions> options = Options.Create(settings);
services.AddSingleton((IOptions<TelegratorOptions>)options);
services.AddSingleton(options);
services.AddSingleton(handlers);
if (handlers is IHandlersManager manager)
{
ServiceDescriptor descriptor = new ServiceDescriptor(typeof(IHandlersProvider), manager);
services.Replace(descriptor);
services.AddSingleton(manager);
}
services.AddTelegramBotHostDefaults();
services.AddTelegramWebhook();
return builder;
}
/// <summary>
/// Replaces the initialization logic from TelegramBotWebHost constructor.
/// Initializes the bot and logs handlers on application startup.
/// </summary>
public static WebApplication UseTelegratorWeb(this WebApplication app)
{
ITelegramBotInfo info = app.Services.GetRequiredService<ITelegramBotInfo>();
IHandlersCollection handlers = app.Services.GetRequiredService<IHandlersCollection>();
ILoggerFactory loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
ILogger logger = loggerFactory.CreateLogger("Telegrator.Hosting.Web.TelegratorHost");
logger.LogInformation("Telegrator Bot ASP.NET WebHost started");
logger.LogHandlers(handlers);
return app;
}
/// <summary>
/// Registers <see cref="ITelegramBotClient"/> service with <see cref="HostedUpdateWebhooker"/> to receive updates using webhook
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
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(ITelegramBotHostBuilder 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, ITelegratorBot
{
}
}
@@ -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>
public 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,10 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0290")]
[assembly: SuppressMessage("Style", "IDE0090")]
[assembly: SuppressMessage("Usage", "CA2254")]
@@ -0,0 +1,30 @@
using Microsoft.Extensions.Configuration;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegrator.Configuration;
namespace Telegrator.Hosting
{
/// <summary>
/// Implementation of <see cref="ITelegramBotInfo"/> that provides bot information.
/// Contains metadata about the Telegram bot including user details and service provider for wider filterring abilities
/// </summary>
/// <param name="client"></param>
/// <param name="services"></param>
/// <param name="configuration"></param>
public class HostedTelegramBotInfo(ITelegramBotClient client, IServiceProvider services, IConfigurationManager configuration) : ITelegramBotInfo
{
/// <inheritdoc/>
public User User { get; } = client.GetMe().Result;
/// <summary>
/// Provides access to services of this Hosted telegram bot
/// </summary>
public IServiceProvider Services { get; } = services;
/// <summary>
/// Provides access to configuration of this Hosted telegram bot
/// </summary>
public IConfigurationManager Configuration { get; } = configuration;
}
}
@@ -0,0 +1,46 @@
using Microsoft.Extensions.Logging;
using Telegrator.Logging;
namespace Telegrator.Hosting.Logging
{
/// <summary>
/// Adapter for Microsoft.Extensions.Logging to work with Telegrator logging system.
/// This allows seamless integration with ASP.NET Core logging infrastructure.
/// </summary>
public class MicrosoftLoggingAdapter : ITelegratorLogger
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of MicrosoftLoggingAdapter.
/// </summary>
/// <param name="logger">The Microsoft.Extensions.Logging logger instance.</param>
public MicrosoftLoggingAdapter(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc/>
public void Log(Telegrator.Logging.LogLevel level, string message, Exception? exception = null)
{
var msLogLevel = level switch
{
Telegrator.Logging.LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace,
Telegrator.Logging.LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
Telegrator.Logging.LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information,
Telegrator.Logging.LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning,
Telegrator.Logging.LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
_ => Microsoft.Extensions.Logging.LogLevel.Information
};
if (exception != null)
{
_logger.Log(msLogLevel, default, message, exception, (str, exc) => string.Format("{0} : {1}", str, exc));
}
else
{
_logger.Log(msLogLevel, default, message, null, (str, _) => str);
}
}
}
}
@@ -0,0 +1,13 @@
using Microsoft.Extensions.Options;
using Telegrator.MadiatorCore;
using Telegrator.Polling;
namespace Telegrator.Hosting.Polling
{
/// <inheritdoc/>
public class HostUpdateHandlersPool(IUpdateRouter router, IOptions<TelegratorOptions> options)
: UpdateHandlersPool(router, options.Value, options.Value.GlobalCancellationToken)
{
}
}
@@ -0,0 +1,60 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using Telegrator.Configuration;
using Telegrator.MadiatorCore;
using Telegrator.Polling;
namespace Telegrator.Hosting.Polling
{
/// <inheritdoc/>
public class HostUpdateRouter : UpdateRouter
{
/// <summary>
/// <see cref="ILogger"/> of this router
/// </summary>
protected readonly ILogger<HostUpdateRouter> Logger;
/// <inheritdoc/>
public HostUpdateRouter(
IHandlersProvider handlersProvider,
IAwaitingProvider awaitingProvider,
IOptions<TelegratorOptions> options,
IUpdateHandlersPool handlersPool,
ITelegramBotInfo botInfo,
ILogger<HostUpdateRouter> logger) : base(handlersProvider, awaitingProvider, options.Value, handlersPool, botInfo)
{
Logger = logger;
ExceptionHandler = new DefaultRouterExceptionHandler(HandleException);
}
/// <inheritdoc/>
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);
}
/// <summary>
/// Default exception handler of this router
/// </summary>
/// <param name="botClient"></param>
/// <param name="exception"></param>
/// <param name="source"></param>
/// <param name="cancellationToken"></param>
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.ToString(),
handlerFaultedException.InnerException?.ToString() ?? "No inner exception");
return;
}
Logger.LogError("Exception was thrown during update routing faulted :\n{exception}", exception.ToString());
}
}
}
@@ -0,0 +1,34 @@
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
{
/// <summary>
/// Service for receiving updates for Hosted telegram bots
/// </summary>
/// <param name="botHost"></param>
/// <param name="botClient"></param>
/// <param name="updateRouter"></param>
/// <param name="options"></param>
/// <param name="logger"></param>
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;
/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Starting receiving updates via long-polling");
_receiverOptions.AllowedUpdates = botHost.UpdateRouter.HandlersProvider.AllowedTypes.ToArray();
DefaultUpdateReceiver updateReceiver = new DefaultUpdateReceiver(botClient, _receiverOptions);
await updateReceiver.ReceiveAsync(_updateRouter, stoppingToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,16 @@
using Telegrator.MadiatorCore;
namespace Telegrator.Hosting.Providers.Components
{
/// <summary>
/// Collection class for managing handler descriptors organized by update type for host apps.
/// Provides functionality for collecting, adding, scanning, and organizing handlers.
/// </summary>
public interface IHostHandlersCollection : IHandlersCollection
{
/// <summary>
/// List of tasks that should be completed right before building the bot
/// </summary>
public List<PreBuildingRoutine> PreBuilderRoutines { get; }
}
}
@@ -0,0 +1,12 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegrator.Providers;
namespace Telegrator.Hosting.Providers
{
/// <inheritdoc/>
public class HostAwaitingProvider(IOptions<TelegratorOptions> options, ILogger<HostAwaitingProvider> logger) : AwaitingProvider(options.Value)
{
private readonly ILogger<HostAwaitingProvider> _logger = logger;
}
}
@@ -0,0 +1,81 @@
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using Telegrator.Configuration;
using Telegrator.Hosting.Components;
using Telegrator.Hosting.Providers.Components;
using Telegrator.MadiatorCore;
using Telegrator.MadiatorCore.Descriptors;
using Telegrator.Providers;
namespace Telegrator.Hosting.Providers
{
/// <summary>
/// Pre host building task
/// </summary>
/// <param name="builder"></param>
public delegate void PreBuildingRoutine(ITelegramBotHostBuilder builder);
/// <inheritdoc/>
public class HostHandlersCollection(IServiceCollection hostServiceColletion, ITelegratorOptions options) : HandlersCollection(options), IHostHandlersCollection
{
private readonly IServiceCollection Services = hostServiceColletion;
/// <inheritdoc/>
protected override bool MustHaveParameterlessCtor => false;
/// <summary>
/// List of tasks that should be completed right before building the bot
/// </summary>
public List<PreBuildingRoutine> PreBuilderRoutines { get; } = [];
/// <inheritdoc/>
public override IHandlersCollection AddDescriptor(HandlerDescriptor descriptor)
{
if (descriptor.HandlerType.IsPreBuildingRoutine(out MethodInfo? routineMethod))
PreBuilderRoutines.Add(routineMethod.CreateDelegate<PreBuildingRoutine>(null));
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,46 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegrator.Handlers.Components;
using Telegrator.MadiatorCore;
using Telegrator.MadiatorCore.Descriptors;
using Telegrator.Providers;
namespace Telegrator.Hosting.Providers
{
/// <inheritdoc/>
public class HostHandlersProvider : HandlersProvider
{
private readonly IServiceProvider Services;
private readonly ILogger<HostHandlersProvider> Logger;
/// <inheritdoc/>
public HostHandlersProvider(
IHandlersCollection handlers,
IOptions<TelegratorOptions> options,
IServiceProvider serviceProvider,
ILogger<HostHandlersProvider> logger) : base(handlers, options.Value)
{
Services = serviceProvider;
Logger = logger;
}
/// <inheritdoc/>
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("Failed to resolve " + descriptor.HandlerType + " as UpdateHandlerBase");
descriptor.LazyInitialization?.Invoke(updateHandler);
updateHandler.LifetimeToken.OnLifetimeEnded += _ => scope.Dispose();
return updateHandler;
}
}
}
+92
View File
@@ -0,0 +1,92 @@
# Telegrator.Hosting
**Telegrator.Hosting** is an extension for the Telegrator framework that provides seamless integration with the .NET Generic Host, enabling production-ready, scalable, and maintainable Telegram bot applications.
---
## Features
- Integration with `Microsoft.Extensions.Hosting` (background services, DI, configuration, logging)
- Automatic handler discovery and registration
- Strongly-typed configuration via `appsettings.json` and environment variables
- Graceful startup/shutdown and lifecycle management
- Advanced error handling and logging
- Supports all Telegrator handler/filter/state features
---
## Requirements
- .NET 8.0 or later
- [Telegrator](https://github.com/Rikitav/Telegrator)
---
## Installation
```shell
dotnet add package Telegrator.Hosting
```
---
## Quick Start Example
**Program.cs:**
```csharp
using Telegrator.Hosting;
// Creating builder
TelegramBotHostBuilder builder = TelegramBotHost.CreateBuilder(new TelegramBotHostBuilderSettings()
{
Args = args,
ExceptIntersectingCommandAliases = true
});
// Registerring handlers
builder.Handlers.CollectHandlersAssemblyWide();
// Register your services
builder.Services.AddSingleton<IMyService, MyService>();
// Building and running application
TelegramBotHost telegramBot = builder.Build();
telegramBot.SetBotCommands();
telegramBot.Run();
```
---
## Configuration (appsettings.json)
```json
{
"TelegramBotClientOptions": {
"Token": "YOUR_BOT_TOKEN"
},
"HostOptions": {
"ShutdownTimeout": 10,
"BackgroundServiceExceptionBehavior": "StopHost"
},
"ReceiverOptions": {
"DropPendingUpdates": true,
"Limit": 10
}
}
```
- `TelegramBotClientOptions`: Bot token and client settings
- `HostOptions`: Host lifecycle and shutdown behavior
- `ReceiverOptions`: Long-polling configuration
---
## Documentation
- [Telegrator Main Docs](https://github.com/Rikitav/Telegrator)
- [Getting Started Guide](https://github.com/Rikitav/Telegrator/wiki/Getting-started)
- [Annotation Overview](https://github.com/Rikitav/Telegrator/wiki/Annotation-overview)
---
## License
GPLv3
+136
View File
@@ -0,0 +1,136 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Telegrator.Hosting.Components;
using Telegrator.MadiatorCore;
namespace Telegrator.Hosting
{
/// <summary>
/// Represents a hosted telegram bot
/// </summary>
public class TelegramBotHost : ITelegramBotHost
{
private readonly IHost _innerHost;
private readonly IServiceProvider _serviceProvider;
private readonly IUpdateRouter _updateRouter;
private readonly ILogger<TelegramBotHost> _logger;
private bool _disposed;
/// <inheritdoc/>
public IServiceProvider Services => _serviceProvider;
/// <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="hostApplicationBuilder">The proxied instance of host builder.</param>
/// <param name="handlers"></param>
public TelegramBotHost(HostApplicationBuilder hostApplicationBuilder, IHandlersCollection handlers)
{
// Registering this host in services for easy access
RegisterHostServices(hostApplicationBuilder.Services, handlers);
// Building proxy hoster
_innerHost = hostApplicationBuilder.Build();
_serviceProvider = _innerHost.Services;
_innerHost.UseTelegrator();
// Reruesting services for this host
_updateRouter = Services.GetRequiredService<IUpdateRouter>();
_logger = Services.GetRequiredService<ILogger<TelegramBotHost>>();
// Logging registering handlers in DEBUG purposes
_logger.LogHandlers(handlers);
}
/// <summary>
/// Creates new <see cref="TelegramBotHostBuilder"/> with default configuration, services and long-polling update receiving scheme
/// </summary>
/// <returns></returns>
public static TelegramBotHostBuilder CreateBuilder()
{
HostApplicationBuilder innerBuilder = new HostApplicationBuilder(settings: null);
TelegramBotHostBuilder builder = new TelegramBotHostBuilder(innerBuilder, null);
builder.Services.AddTelegramBotHostDefaults();
builder.Services.AddTelegramReceiver();
return builder;
}
/// <summary>
/// Creates new <see cref="TelegramBotHostBuilder"/> with default services and long-polling update receiving scheme
/// </summary>
/// <returns></returns>
public static TelegramBotHostBuilder CreateBuilder(TelegramBotHostBuilderSettings? settings)
{
HostApplicationBuilder innerBuilder = new HostApplicationBuilder(settings?.ToApplicationBuilderSettings());
TelegramBotHostBuilder builder = new TelegramBotHostBuilder(innerBuilder, settings);
builder.Services.AddTelegramBotHostDefaults();
builder.Services.AddTelegramReceiver();
return builder;
}
/// <summary>
/// Creates new EMPTY <see cref="TelegramBotHostBuilder"/> WITHOUT any services or update receiving schemes
/// </summary>
/// <returns></returns>
public static TelegramBotHostBuilder CreateEmptyBuilder()
{
HostApplicationBuilder innerBuilder = Host.CreateEmptyApplicationBuilder(null);
return new TelegramBotHostBuilder(innerBuilder, null);
}
/// <summary>
/// Creates new EMPTY <see cref="TelegramBotHostBuilder"/> WITHOUT any services or update receiving schemes
/// </summary>
/// <returns></returns>
public static TelegramBotHostBuilder CreateEmptyBuilder(TelegramBotHostBuilderSettings? settings)
{
HostApplicationBuilder innerBuilder = Host.CreateEmptyApplicationBuilder(null);
return new TelegramBotHostBuilder(innerBuilder, settings);
}
/// <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;
_innerHost.Dispose();
GC.SuppressFinalize(this);
_disposed = true;
}
private void RegisterHostServices(IServiceCollection services, IHandlersCollection handlers)
{
//services.RemoveAll<IHost>();
//services.AddSingleton<IHost>(this);
services.AddSingleton<ITelegramBotHost>(this);
services.AddSingleton<ITelegratorBot>(this);
}
}
}
@@ -0,0 +1,76 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Telegrator.Hosting.Components;
using Telegrator.Hosting.Providers;
using Telegrator.MadiatorCore;
#pragma warning disable IDE0001
namespace Telegrator.Hosting
{
/// <summary>
/// Represents a hosted telegram bots and services builder that helps manage configuration, logging, lifetime, and more.
/// </summary>
public class TelegramBotHostBuilder : ITelegramBotHostBuilder
{
private readonly HostApplicationBuilder _innerBuilder;
private readonly TelegramBotHostBuilderSettings _settings;
private readonly IHandlersCollection _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>
/// <param name="hostApplicationBuilder"></param>
/// <param name="settings"></param>
public TelegramBotHostBuilder(HostApplicationBuilder hostApplicationBuilder, TelegramBotHostBuilderSettings? settings = null)
{
_innerBuilder = hostApplicationBuilder ?? throw new ArgumentNullException(nameof(hostApplicationBuilder));
_settings = settings ?? new TelegramBotHostBuilderSettings();
_handlers = new HostHandlersCollection(Services, _settings);
_innerBuilder.AddTelegrator(_settings, _handlers);
_innerBuilder.Logging.ClearProviders();
}
/// <summary>
/// Initializes a new instance of the <see cref="TelegramBotHostBuilder"/> class.
/// </summary>
/// <param name="hostApplicationBuilder"></param>
/// <param name="handlers"></param>
/// <param name="settings"></param>
public TelegramBotHostBuilder(HostApplicationBuilder hostApplicationBuilder, IHandlersCollection handlers, TelegramBotHostBuilderSettings? settings = null)
{
_innerBuilder = hostApplicationBuilder ?? throw new ArgumentNullException(nameof(hostApplicationBuilder));
_settings = settings ?? new TelegramBotHostBuilderSettings();
_handlers = handlers ?? throw new ArgumentNullException(nameof(handlers));
_innerBuilder.AddTelegrator(_settings, _handlers);
_innerBuilder.Logging.ClearProviders();
}
/// <summary>
/// Builds the host.
/// </summary>
/// <returns></returns>
public TelegramBotHost Build()
{
return new TelegramBotHost(_innerBuilder, _handlers);
}
}
}
@@ -0,0 +1,45 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
namespace Telegrator.Hosting
{
/// <summary>
/// Settings os hosted Telegram bot
/// </summary>
public class TelegramBotHostBuilderSettings() : TelegratorOptions
{
/// <summary>
/// Disables automatic configuration for all of required <see cref="IOptions{TOptions}"/> instances
/// </summary>
public bool DisableAutoConfigure { get; set; }
/// <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; }
internal HostApplicationBuilderSettings ToApplicationBuilderSettings() => new HostApplicationBuilderSettings()
{
DisableDefaults = DisableDefaults,
Args = Args,
Configuration = Configuration,
EnvironmentName = EnvironmentName,
ApplicationName = ApplicationName,
ContentRootPath = ContentRootPath
};
}
}
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<BaseOutputPath>..\..\bin</BaseOutputPath>
<DocumentationFile>..\..\docs\$(AssemblyName).xml</DocumentationFile>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<EnableNETAnalyzers>True</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<Title>Telegrator.Hosting</Title>
<Version>1.16.0</Version>
<Authors>Rikitav Tim4ik</Authors>
<Company>Rikitav Tim4ik</Company>
<RepositoryUrl>https://github.com/Rikitav/Telegrator</RepositoryUrl>
<PackageTags>telegram;bot;mediator;attributes;aspect;hosting;host;framework;easy;simple;handlers</PackageTags>
<PackageIcon>telegrator_nuget.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.3" />
</ItemGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="True" PackagePath="\" />
<None Include="..\README.md" Pack="True" PackagePath="\" />
<None Include="..\resources\telegrator_nuget.png" Pack="True" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Telegrator\Telegrator.csproj" />
</ItemGroup>
</Project>
+250
View File
@@ -0,0 +1,250 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegrator.Configuration;
using Telegrator.Hosting.Components;
using Telegrator.Hosting.Configuration;
using Telegrator.Hosting.Logging;
using Telegrator.Hosting.Polling;
using Telegrator.Hosting.Providers;
using Telegrator.Hosting.Providers.Components;
using Telegrator.Logging;
using Telegrator.MadiatorCore;
using Telegrator.MadiatorCore.Descriptors;
namespace Telegrator.Hosting
{
public static class HostBuilderExtensions
{
/// <summary>
/// Replaces TelegramBotWebHostBuilder. Configures DI, options, and handlers.
/// </summary>
public static IHostApplicationBuilder AddTelegrator(this IHostApplicationBuilder builder, TelegramBotHostBuilderSettings settings, IHandlersCollection? handlers = null)
{
if (settings is null)
throw new ArgumentNullException(nameof(settings));
IServiceCollection services = builder.Services;
IConfigurationManager configuration = builder.Configuration;
handlers ??= new HostHandlersCollection(services, settings);
if (handlers is IHostHandlersCollection hostHandlers)
{
foreach (PreBuildingRoutine preBuildRoutine in hostHandlers.PreBuilderRoutines)
{
try
{
// TODO: fix
//preBuildRoutine.Invoke(builder);
Debug.WriteLine("Pre-Building routine was not executed");
}
catch (NotImplementedException)
{
_ = 0xBAD + 0xC0DE;
}
}
}
if (!settings.DisableAutoConfigure)
{
services.Configure<ReceiverOptions>(configuration.GetSection(nameof(ReceiverOptions)));
services.Configure(configuration.GetSection(nameof(TelegramBotClientOptions)), new TelegramBotClientOptionsProxy());
}
else
{
/*
if (null == Services.SingleOrDefault(srvc => srvc.ImplementationType == typeof(IOptions<ReceiverOptions>)))
throw new MissingMemberException("Auto configuration disabled, yet no options of type 'ReceiverOptions' wasn't registered. This configuration is runtime required!");
*/
if (null == services.SingleOrDefault(srvc => srvc.ImplementationType == typeof(IOptions<TelegramBotClientOptions>)))
throw new MissingMemberException("Auto configuration disabled, yet no options of type 'TelegramBotClientOptions' wasn't registered. This configuration is runtime required!");
}
IOptions<TelegramBotHostBuilderSettings> options = Options.Create(settings);
services.AddSingleton((IOptions<TelegratorOptions>)options);
services.AddTelegramBotHostDefaults();
services.AddSingleton(options);
services.AddSingleton(handlers);
if (handlers is IHandlersManager manager)
{
ServiceDescriptor descriptor = new ServiceDescriptor(typeof(IHandlersProvider), manager);
services.Replace(descriptor);
services.AddSingleton(manager);
}
return builder;
}
}
/// <summary>
/// Contains extensions for <see cref="IServiceCollection"/>
/// Provides method to configure <see cref="ITelegramBotHost"/>
/// </summary>
public static class ServicesCollectionExtensions
{
/// <summary>
/// Registers a configuration instance that strongly-typed <typeparamref name="TOptions"/> will bind against using <see cref="ConfigureOptionsProxy{TOptions}"/>.
/// </summary>
/// <typeparam name="TOptions"></typeparam>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <param name="optionsProxy"></param>
/// <returns></returns>
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration configuration, ConfigureOptionsProxy<TOptions> optionsProxy) where TOptions : class
{
optionsProxy.Configure(services, configuration);
return services;
}
/// <summary>
/// Registers <see cref="TelegramBotHost"/> default services
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddTelegramBotHostDefaults(this IServiceCollection services)
{
services.AddLogging(builder => builder.AddConsole().AddDebug());
services.AddSingleton<IUpdateHandlersPool, HostUpdateHandlersPool>();
services.AddSingleton<IAwaitingProvider, HostAwaitingProvider>();
services.AddSingleton<IHandlersProvider, HostHandlersProvider>();
services.AddSingleton<IUpdateRouter, HostUpdateRouter>();
services.AddSingleton<ITelegramBotInfo, HostedTelegramBotInfo>();
return services;
}
/// <summary>
/// Registers <see cref="ITelegramBotClient"/> service with <see cref="HostedUpdateReceiver"/> to receive updates using long polling
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddTelegramReceiver(this IServiceCollection services)
{
services.AddHttpClient<ITelegramBotClient>("tgreceiver").RemoveAllLoggers().AddTypedClient(TypedTelegramBotClientFactory);
services.AddHostedService<HostedUpdateReceiver>();
return services;
}
/// <summary>
/// <see cref="ITelegramBotClient"/> factory method
/// </summary>
/// <param name="httpClient"></param>
/// <param name="provider"></param>
/// <returns></returns>
private static ITelegramBotClient TypedTelegramBotClientFactory(HttpClient httpClient, IServiceProvider provider)
=> new TelegramBotClient(provider.GetRequiredService<IOptions<TelegramBotClientOptions>>().Value, httpClient);
}
/// <summary>
/// Provides useful methods to adjust <see cref="ITelegramBotHost"/>
/// </summary>
public static class TelegramBotHostExtensions
{
/// <summary>
/// Replaces the initialization logic from TelegramBotWebHost constructor.
/// Initializes the bot and logs handlers on application startup.
/// </summary>
public static IHost UseTelegrator(this IHost botHost)
{
ITelegramBotInfo info = botHost.Services.GetRequiredService<ITelegramBotInfo>();
IHandlersCollection handlers = botHost.Services.GetRequiredService<IHandlersCollection>();
ILoggerFactory loggerFactory = botHost.Services.GetRequiredService<ILoggerFactory>();
ILogger logger = loggerFactory.CreateLogger("Telegrator.Hosting.Web.TelegratorHost");
logger.LogInformation("Telegrator Bot .NET Host started");
logger.LogHandlers(handlers);
return botHost;
}
/// <summary>
/// Configures bots available commands depending on what handlers was registered
/// </summary>
/// <param name="botHost"></param>
/// <returns></returns>
public static IHost SetBotCommands(this IHost botHost)
{
ITelegramBotClient client = botHost.Services.GetRequiredService<ITelegramBotClient>();
IUpdateRouter router = botHost.Services.GetRequiredService<IUpdateRouter>();
IEnumerable<BotCommand> aliases = router.HandlersProvider.GetBotCommands();
client.SetMyCommands(aliases).Wait();
return botHost;
}
/// <summary>
/// Adds a Microsoft.Extensions.Logging adapter to Alligator using a logger factory.
/// </summary>
/// <param name="host"></param>
public static IHost AddLoggingAdapter(this IHost host)
{
ILoggerFactory loggerFactory = host.Services.GetRequiredService<ILoggerFactory>();
ILogger logger = loggerFactory.CreateLogger("Telegrator");
MicrosoftLoggingAdapter adapter = new MicrosoftLoggingAdapter(logger);
Alligator.AddAdapter(adapter);
return host;
}
}
/// <summary>
/// Provides extension methods for reflection and type inspection.
/// </summary>
public static class ReflectionExtensions
{
/// <summary>
/// Checks if a type implements the <see cref="IPreBuildingRoutine"/> interface.
/// </summary>
/// <param name="handlerType">The type to check.</param>
/// <param name="routineMethod"></param>
/// <returns>True if the type implements IPreBuildingRoutine; otherwise, false.</returns>
public static bool IsPreBuildingRoutine(this Type handlerType, [NotNullWhen(true)] out MethodInfo? routineMethod)
{
routineMethod = null;
if (handlerType.GetInterface(nameof(IPreBuildingRoutine)) == null)
return false;
routineMethod = handlerType.GetMethod(nameof(IPreBuildingRoutine.PreBuildingRoutine), BindingFlags.Static | BindingFlags.Public);
return routineMethod != null;
}
}
public static class LoggerExtensions
{
public static void LogHandlers(this ILogger logger, IHandlersCollection 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.Append("\n\tUpdateType." + updateType + " :");
foreach (HandlerDescriptor descriptor in descriptors.Reverse())
{
logBuilder.AppendFormat("\n\t* {0} - {1}",
descriptor.Indexer.ToString(),
descriptor.ToString());
}
}
logger.LogInformation(logBuilder.ToString());
}
}
}
@@ -0,0 +1,11 @@
using Microsoft.Extensions.Localization;
using Telegram.Bot.Types;
using Telegrator.Handlers.Components;
namespace Telegrator.Localized
{
public interface ILocalizedHandler<T> : IAbstractUpdateHandler<Message> where T : class
{
public IStringLocalizer LocalizationProvider { get; }
}
}
@@ -0,0 +1,10 @@
using Telegram.Bot.Types;
using Telegrator.Handlers.Components;
namespace Telegrator.Localized
{
public interface ILocalizedMessageHandler : ILocalizedHandler<Message>
{
}
}
@@ -0,0 +1,17 @@
using Microsoft.Extensions.Localization;
using System.Collections.Generic;
using System.Threading.Tasks;
using Telegram.Bot.Types;
using Telegrator.Handlers;
namespace Telegrator.Localized
{
public static class LocalizedMessageHandlerExtensions
{
public static async Task<Message> ResponseLocalized(this ILocalizedHandler<Message> localizedHandler, string localizedReplyIdentifier, params IEnumerable<string> formatArgs)
{
LocalizedString localizedString = localizedHandler.LocalizationProvider[localizedReplyIdentifier, formatArgs];
return await localizedHandler.Container.Responce(localizedString.Value);
}
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="10.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Telegrator\Telegrator.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,44 @@
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegrator.Attributes;
using Telegrator.Filters;
using Telegrator.Filters.Components;
namespace Telegrator.Annotations
{
/// <summary>
/// Abstract base attribute for filtering callback-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 CallbackQueryAttribute(params IFilter<CallbackQuery>[] filters) : UpdateFilterAttribute<CallbackQuery>(filters)
{
/// <summary>
/// Gets the allowed update types that this filter can process.
/// </summary>
public override UpdateType[] AllowedTypes => [UpdateType.CallbackQuery];
/// <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 CallbackQuery? GetFilterringTarget(Update update)
=> update.CallbackQuery;
}
/// <summary>
/// Attribute for filtering <see cref="CallbackQuery"/>'s data
/// </summary>
/// <param name="data"></param>
public class CallbackDataAttribute(string data)
: CallbackQueryAttribute(new CallbackDataFilter(data))
{ }
/// <summary>
/// Attribute to check if <see cref="CallbackQuery"/> belongs to a specific message by its ID
/// </summary>
public class CallbackInlineIdAttribute(string inlineMessageId)
: CallbackQueryAttribute(new CallbackInlineIdFilter(inlineMessageId))
{ }
}
@@ -0,0 +1,60 @@
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.Select(c => c.TrimStart('/')).ToArray()))
=> Alliases = alliases.Select(c => c.TrimStart('/')).ToArray();
/// <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,63 @@
using System.Text.RegularExpressions;
using Telegrator.Filters;
namespace Telegrator.Annotations
{
/// <summary>
/// Attribute for filtering messages where a command has arguments count >= <paramref name="count"/>.
/// </summary>
/// <param name="count"></param>
public class ArgumentCountAttribute(int count)
: MessageFilterAttribute(new ArgumentCountFilter(count))
{ }
/// <summary>
/// Attribute for filtering messages where a command argument starts with the specified content.
/// </summary>
/// <param name="content">The content that the command argument should start with.</param>
/// <param name="comparison">The string comparison type to use for the check.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentStartsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0)
: MessageFilterAttribute(new ArgumentStartsWithFilter(content, comparison, index))
{ }
/// <summary>
/// Attribute for filtering messages where a command argument ends with the specified content.
/// </summary>
/// <param name="content">The content that the command argument should end with.</param>
/// <param name="comparison">The string comparison type to use for the check.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentEndsWithAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0)
: MessageFilterAttribute(new ArgumentEndsWithFilter(content, comparison, index))
{ }
/// <summary>
/// Attribute for filtering messages where a command argument contains the specified content.
/// </summary>
/// <param name="content">The content that the command argument should contain.</param>
/// <param name="comparison">The string comparison type to use for the check.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentContainsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0)
: MessageFilterAttribute(new ArgumentContainsFilter(content, comparison, index))
{ }
/// <summary>
/// Attribute for filtering messages where a command argument equals the specified content.
/// </summary>
/// <param name="content">The content that the command argument should equal.</param>
/// <param name="comparison">The string comparison type to use for the check.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentEqualsAttribute(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0)
: MessageFilterAttribute(new ArgumentEqualsFilter(content, comparison, index))
{ }
/// <summary>
/// Attribute for filtering messages where a command argument matches a regular expression pattern.
/// </summary>
/// <param name="pattern">The regular expression pattern to match against the command argument.</param>
/// <param name="options">The regex options to use for the pattern matching.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentRegexAttribute(string pattern, RegexOptions options = RegexOptions.None, int index = 0)
: MessageFilterAttribute(new ArgumentRegexFilter(pattern, options, index: index))
{ }
}
@@ -0,0 +1,12 @@
namespace Telegrator.Annotations
{
/// <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,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, null, 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,105 @@
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>
public class ChatTypeAttribute : MessageFilterAttribute
{
/// <summary>
/// Initialize new instance of <see cref="ChatTypeAttribute"/> to filter messages from chat from specific chats
/// </summary>
/// <param name="type"></param>
public ChatTypeAttribute(ChatType type)
: base(new MessageChatTypeFilter(type)) { }
/// <summary>
/// Initialize new instance of <see cref="ChatTypeAttribute"/> to filter messages from chat from specific chats (with flags)
/// </summary>
/// <param name="flags"></param>
public ChatTypeAttribute(ChatTypeFlags flags)
: base(new MessageChatTypeFilter(flags)) { }
}
/// <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,27 @@
using Telegrator.Filters;
namespace Telegrator.Annotations
{
/// <summary>
/// Attribute for filtering messages with reply to messages of this bot.
/// </summary>
public class MeRepliedAttribute()
: MessageFilterAttribute(new MeRepliedFilter())
{ }
/// <summary>
/// Attribute for checking message's reply chain.
/// </summary>
public class HasReplyAttribute(int replyDepth = 1)
: MessageFilterAttribute(new MessageHasReplyFilter(replyDepth))
{ }
/// <summary>
/// Helper filter class for filters that operate on replied messages.
/// Provides functionality to traverse reply chains and access replied message content.
/// </summary>
/// <param name="replyDepth"></param>
public class FromReplyChainAttribute(int replyDepth = 1)
: MessageFilterAttribute(new FromReplyChainFilter(replyDepth))
{ }
}
@@ -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,58 @@
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())
{ }
/// <summary>
/// Attribute for filtering messages where the text contains a 'word'.
/// 'Word' must be a separate member of the text, and not have any alphabetic characters next to it.
/// </summary>
/// <param name="word"></param>
/// <param name="comparison"></param>
/// <param name="startIndex"></param>
public class TextContainsWordAttribute(string word, StringComparison comparison = StringComparison.InvariantCulture, int startIndex = 0)
: MessageFilterAttribute(new TextContainsWordFilter(word, comparison, startIndex))
{ }
}
@@ -0,0 +1,27 @@
using Telegram.Bot.Types.Enums;
namespace Telegrator.Annotations
{
/// <summary>
/// Attribute that says if this handler can await some of await types, that is not listed by its handler base.
/// Used for automatic collecting allowed to receiving <see cref="UpdateType"/>'s.
/// If you don't use it, you won't be able to await the updates inside handler.
/// </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,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,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 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()) { }
}
}
@@ -0,0 +1,23 @@
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegrator.Filters;
namespace Telegrator.Annotations.Targetted
{
/// <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>
/// <param name="onlyFirst"></param>
public WelcomeAttribute(bool onlyFirst = false) : base(
new MessageChatTypeFilter(ChatType.Private),
new CommandAlliasFilter("start"),
Filter<Message>.If(ctx => !onlyFirst || ctx.Input.Id == 0))
{ }
}
}
@@ -0,0 +1,16 @@
namespace Telegrator.Aspects
{
/// <summary>
/// Attribute that specifies a post-execution processor to be executed after the handler.
/// The processor type must implement <see cref="IPostProcessor"/> interface.
/// </summary>
/// <typeparam name="T">The type of the post-processor that implements <see cref="IPostProcessor"/>.</typeparam>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class AfterExecutionAttribute<T> : Attribute where T : IPostProcessor
{
/// <summary>
/// Gets the type of the post-processor.
/// </summary>
public Type ProcessorType => typeof(T);
}
}
@@ -0,0 +1,16 @@
namespace Telegrator.Aspects
{
/// <summary>
/// Attribute that specifies a pre-execution processor to be executed before the handler.
/// The processor type must implement <see cref="IPreProcessor"/> interface.
/// </summary>
/// <typeparam name="T">The type of the pre-processor that implements <see cref="IPreProcessor"/>.</typeparam>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class BeforeExecutionAttribute<T> : Attribute where T : IPreProcessor
{
/// <summary>
/// Gets the type of the pre-processor.
/// </summary>
public Type ProcessorType => typeof(T);
}
}
@@ -0,0 +1,19 @@
using Telegrator.Handlers.Components;
namespace Telegrator.Aspects
{
/// <summary>
/// Interface for post-execution processors that are executed after handler execution.
/// Implement this interface to add cross-cutting concerns like logging, cleanup, or metrics collection.
/// </summary>
public interface IPostProcessor
{
/// <summary>
/// Executes after the handler's main execution logic.
/// </summary>
/// <param name="container">The handler container containing the current update and context.</param>
/// <param name="cancellationToken"></param>
/// <returns>A <see cref="Result"/> indicating the final execution result.</returns>
public Task<Result> AfterExecution(IHandlerContainer container, CancellationToken cancellationToken);
}
}
+19
View File
@@ -0,0 +1,19 @@
using Telegrator.Handlers.Components;
namespace Telegrator.Aspects
{
/// <summary>
/// Interface for pre-execution processors that are executed before handler execution.
/// Implement this interface to add cross-cutting concerns like validation, logging, or authorization.
/// </summary>
public interface IPreProcessor
{
/// <summary>
/// Executes before the handler's main execution logic.
/// </summary>
/// <param name="container">The handler container containing the current update and context.</param>
/// <param name="cancellationToken"></param>
/// <returns>A <see cref="Result"/> indicating whether execution should continue or be stopped.</returns>
public Task<Result> BeforeExecution(IHandlerContainer container, CancellationToken cancellationToken = default);
}
}
@@ -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 => GetType().HasPublicProperties();
/// <summary>
/// Creates a new instance <see cref="StateKeeperBase{TKey, TState}"/>
/// </summary>
/// <param name="stateKeeperType"></param>
/// <exception cref="ArgumentException"></exception>
protected StateKeeperAttributeBase(Type stateKeeperType)
{
if (!stateKeeperType.IsAssignableToGenericType(typeof(StateKeeperBase<,>)))
throw new ArgumentException(stateKeeperType + " is not a StateKeeperBase", nameof(stateKeeperType));
}
/// <summary>
/// Realizes a <see cref="IFilter{T}"/> for validation of the current <see cref="StateKeeperBase{TKey, TState}"/> in the polling routing
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public abstract bool CanPass(FilterExecutionContext<Update> context);
}
}
@@ -0,0 +1,46 @@
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
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 IFilter<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,82 @@
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 => GetType().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 importance of this <see cref="UpdateHandlerBase"/> in same <see cref="UpdateType"/> pool
/// </summary>
public int Importance { get; set; }
/// <summary>
/// Gets or sets priority of this <see cref="UpdateHandlerBase"/> in same type handlers pool
/// </summary>
public int Priority { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to form a fallback report.
/// </summary>
public bool FormReport { get; set; }
/// <summary>
/// Creates a new instance of <see cref="UpdateHandlerAttributeBase"/>
/// </summary>
/// <param name="expectingHandlerType">The types of handlers that this attribute can be applied to.</param>
/// <param name="updateType">The type of update that this handler processes.</param>
/// <param name="importance">The importance level of this handler (default: 0).</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="expectingHandlerType"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when one of the handler types is not a valid handler type.</exception>
/// <exception cref="Exception">Thrown when <paramref name="updateType"/> is <see cref="UpdateType.Unknown"/>.</exception>
protected internal UpdateHandlerAttributeBase(Type[] expectingHandlerType, UpdateType updateType, int importance = 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;
Importance = importance;
}
/// <summary>
/// Gets an <see cref="DescriptorIndexer"/> of this <see cref="UpdateHandlerAttributeBase"/> from <see cref="Importance"/> and <see cref="Priority"/>
/// </summary>
/// <returns>A descriptor indexer for this handler attribute.</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">The filter execution context containing the update to validate.</param>
/// <returns>True if the update passes validation; otherwise, false.</returns>
public abstract bool CanPass(FilterExecutionContext<Update> context);
}
}
@@ -0,0 +1,39 @@
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegrator.Filters;
using Telegrator.Filters.Components;
namespace Telegrator.Attributes
{
/// <summary>
/// Reactive way to implement a new <see cref="UpdateFilterAttribute{T}"/> of type <typeparamref name="T"/>
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class FilterAnnotation<T> : UpdateFilterAttribute<T>, IFilter<T>, INamedFilter where T : class
{
/// <inheritdoc/>
public virtual bool IsCollectible { get; } = false;
/// <inheritdoc/>
public override UpdateType[] AllowedTypes { get; } = typeof(T).GetAllowedUpdateTypes();
/// <inheritdoc/>
public string Name => GetType().Name;
/// <summary>
/// Initializes new instance of <see cref="FilterAnnotation{T}"/>
/// </summary>
public FilterAnnotation() : base()
{
UpdateFilter = Filter<T>.If(CanPass);
AnonymousFilter = AnonymousTypeFilter.Compile(UpdateFilter, GetFilterringTarget);
}
/// <inheritdoc/>
public override T? GetFilterringTarget(Update update)
=> update.GetActualUpdateObject<T>();
/// <inheritdoc/>
public abstract bool CanPass(FilterExecutionContext<T> context);
}
}
@@ -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,86 @@
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()
{
/*
private static readonly TKeeper _shared = new TKeeper();
private static readonly Dictionary<TKey, TKeeper> _keyed = [];
*/
/// <summary>
/// Gets or sets the singleton instance of the state keeper for this attribute type.
/// </summary>
public static TKeeper Shared { get; } = new TKeeper();
/// <summary>
/// Gets the default state value of this statekeeper.
/// </summary>
public static TState DefaultState => Shared.DefaultState;
/// <summary>
/// Gets the state value associated with this attribute instance.
/// </summary>
public TState MyState { get; private set; }
/// <summary>
/// Gets the special state mode for this attribute instance.
/// </summary>
public SpecialState SpecialState { get; private set; }
/// <summary>
/// Initializes the attribute with a specific state and a custom key resolver.
/// </summary>
/// <param name="myState">The state value to associate</param>
/// <param name="keyResolver">The key resolver for state keeping</param>
protected StateKeeperAttribute(TState myState, IStateKeyResolver<TKey> keyResolver) : base(typeof(TKeeper))
{
Shared.KeyResolver = keyResolver;
MyState = myState;
SpecialState = SpecialState.None;
}
/// <summary>
/// Initializes the attribute with a special state and a custom key resolver.
/// </summary>
/// <param name="specialState">The special state mode</param>
/// <param name="keyResolver">The key resolver for state keeping</param>
protected StateKeeperAttribute(SpecialState specialState, IStateKeyResolver<TKey> keyResolver) : base(typeof(TKeeper))
{
Shared.KeyResolver = keyResolver;
MyState = Shared.DefaultState;
SpecialState = specialState;
}
/// <summary>
/// Determines whether the current update context passes the state filter.
/// </summary>
/// <param name="context">The filter execution context</param>
/// <returns>True if the state matches the filter; otherwise, false.</returns>
public override bool CanPass(FilterExecutionContext<Update> context)
{
if (SpecialState == SpecialState.AnyState)
return true;
if (!Shared.TryGetState(context.Input, out TState? state))
return SpecialState == SpecialState.NoState;
if (state == null)
return false;
return MyState.Equals(state);
}
}
}
@@ -0,0 +1,85 @@
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 IFilter<Update> AnonymousFilter { get; protected set; }
/// <summary>
/// Gets the compiled filter logic for the update target.
/// </summary>
public IFilter<T> UpdateFilter { get; protected set; }
/// <summary>
/// Empty constructor for internal using
/// </summary>
internal UpdateFilterAttribute()
{
AnonymousFilter = null!;
UpdateFilter = null!;
_ = 0xBAD + 0xC0DE;
}
/// <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)
{
string name = GetType().Name;
UpdateFilter = new CompiledFilter<T>(name, filters);
AnonymousFilter = AnonymousTypeFilter.Compile(name, UpdateFilter, GetFilterringTarget);
}
/// <summary>
/// Initializes the attribute with a precompiled filter for the update target.
/// </summary>
/// <param name="updateFilter">The compiled filter</param>
protected UpdateFilterAttribute(IFilter<T> updateFilter)
{
string name = GetType().Name;
UpdateFilter = updateFilter;
AnonymousFilter = AnonymousTypeFilter.Compile(name, 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 = Filter<T>.Not(AnonymousFilter);
if (previous is not null)
{
if (previous.Modifiers.HasFlag(FilterModifier.OrNext))
{
AnonymousFilter = Filter<Update>.Or(previous.AnonymousFilter, 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,46 @@
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 importance settings.
/// </summary>
/// <typeparam name="T">The type of the update handler that this attribute is applied to.</typeparam>
public abstract class UpdateHandlerAttribute<T> : UpdateHandlerAttributeBase where T : UpdateHandlerBase
{
/// <summary>
/// Initializes new instance of <see cref="UpdateHandlerAttribute{T}"/>
/// </summary>
/// <param name="updateType">The type of update that this handler can process.</param>
protected UpdateHandlerAttribute(UpdateType updateType)
: base([typeof(T)], updateType, 0) { }
/// <summary>
/// Initializes new instance of <see cref="UpdateHandlerAttribute{T}"/>
/// </summary>
/// <param name="updateType">The type of update that this handler can process.</param>
/// <param name="importance">The importance level for this handler</param>
protected UpdateHandlerAttribute(UpdateType updateType, int importance)
: base([typeof(T)], updateType, importance) { }
/// <summary>
/// Initializes new instance of <see cref="UpdateHandlerAttribute{T}"/>
/// </summary>
/// <param name="types">Additional suported types.</param>
/// <param name="updateType">The type of update that this handler can process.</param>
protected UpdateHandlerAttribute(Type[] types, UpdateType updateType)
: base([..types, typeof(T)], updateType, 0) { }
/// <summary>
/// Initializes new instance of <see cref="UpdateHandlerAttribute{T}"/>
/// </summary>
/// <param name="types">Additional suported types.</param>
/// <param name="updateType">The type of update that this handler can process.</param>
/// <param name="importance">The importance level for this handler</param>
protected UpdateHandlerAttribute(Type[] types, UpdateType updateType, int importance)
: base([.. types, typeof(T)], updateType, importance) { }
}
}
@@ -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>
/// Interface for configuring Telegram bot behavior and execution settings.
/// Controls various aspects of bot operation including concurrency, routing, collecting, and execution policies.
/// </summary>
public interface ITelegratorOptions
{
/// <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 a value indicating whether to exclude intersecting command aliases.
/// </summary>
public bool ExceptIntersectingCommandAliases { get; set; }
/// <summary>
/// Gets or sets the global cancellation token for all bot operations.
/// </summary>
public CancellationToken GlobalCancellationToken { get; set; }
}
}
+134
View File
@@ -0,0 +1,134 @@
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
}
/// <summary>
/// Flags version of <see cref="Telegram.Bot.Types.Enums.ChatType"/>
/// Type of the <see cref="Telegram.Bot.Types.Chat"/>, from which the message or inline query was sent
/// </summary>
[Flags]
public enum ChatTypeFlags
{
/// <summary>
/// Normal one-to-one chat with a user or bot
/// </summary>
Private = 0x1,
/// <summary>
/// Normal group chat
/// </summary>
Group = 0x2,
/// <summary>
/// A channel
/// </summary>
Channel = 0x4,
/// <summary>
/// A supergroup
/// </summary>
Supergroup = 0x8,
/// <summary>
/// Value possible only in <see cref="Telegram.Bot.Types.InlineQuery.ChatType"/>: private chat with the inline query sender
/// </summary>
Sender
}
/*
/// <summary>
/// Messages from where this filter was originated
/// </summary>
public enum FilterOrigin
{
/// <summary>
/// None, empty filter
/// </summary>
None,
/// <summary>
/// From <see cref="Attributes.UpdateHandlerAttribute{T}"/> update validator filter
/// </summary>
Validator,
/// <summary>
/// From <see cref="Attributes.StateKeeperAttribute{TKey, TState, TKeeper}"/> state machine filter
/// </summary>
StateKeeper,
/// <summary>
/// From regular <see cref="Attributes.UpdateFilterAttribute{T}"/>
/// </summary>
Regualr
}
*/
/*
/// <summary>
/// Levels of debug writing
/// </summary>
[Flags]
public enum DebugLevel
{
/// <summary>
/// None to write
/// </summary>
None = 0x0,
/// <summary>
/// Write debug messages from filters execution
/// </summary>
Filters = 0x1,
/// <summary>
/// Write debug messages from handlers providers execution
/// </summary>
Providers = 0x2,
/// <summary>
/// Write debug messages from update router's execution
/// </summary>
Router = 0x4,
/// <summary>
/// Write debug messages from handlers pool execution
/// </summary>
HandlersPool = 0x8
}
*/
}
+52
View File
@@ -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 DescribedHandlerDescriptor 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(DescribedHandlerDescriptor handlerInfo, Exception inner)
: base(string.Format("Handler's \"{0}\" execution was faulted", handlerInfo.DisplayString), inner)
{
HandlerInfo = handlerInfo;
}
}
}
@@ -0,0 +1,73 @@
using System.Text.RegularExpressions;
using Telegram.Bot.Types;
using Telegrator.Filters.Components;
namespace Telegrator.Filters
{
/// <summary>
/// Filter thet checks <see cref="CallbackQuery"/>'s data
/// </summary>
public class CallbackDataFilter : Filter<CallbackQuery>
{
private readonly string _data;
/// <summary>
/// Initialize new instance of <see cref="CallbackDataFilter"/>
/// </summary>
/// <param name="data"></param>
public CallbackDataFilter(string data)
{
_data = data;
}
/// <inheritdoc/>
public override bool CanPass(FilterExecutionContext<CallbackQuery> context)
{
return context.Input.Data == _data;
}
}
/// <summary>
/// Filter that checks if <see cref="CallbackQuery"/> belongs to a specific message
/// </summary>
public class CallbackInlineIdFilter : Filter<CallbackQuery>
{
private readonly string _inlineMessageId;
/// <summary>
/// Initialize new instance of <see cref="CallbackInlineIdFilter"/>
/// </summary>
/// <param name="inlineMessageId"></param>
public CallbackInlineIdFilter(string inlineMessageId)
{
_inlineMessageId = inlineMessageId;
}
/// <inheritdoc/>
public override bool CanPass(FilterExecutionContext<CallbackQuery> context)
{
return context.Input.InlineMessageId == _inlineMessageId;
}
}
/// <summary>
/// Filters callback queries by matching their data with a regular expression.
/// </summary>
public class CallbackRegexFilter : RegexFilterBase<CallbackQuery>
{
/// <summary>
/// Initializes a new instance of the <see cref="CallbackRegexFilter"/> class with a pattern and options.
/// </summary>
/// <param name="pattern">The regex pattern.</param>
/// <param name="regexOptions">The regex options.</param>
public CallbackRegexFilter(string pattern, RegexOptions regexOptions = default)
: base(clb => clb.Data, pattern, regexOptions) { }
/// <summary>
/// Initializes a new instance of the <see cref="CallbackRegexFilter"/> class with a regex object.
/// </summary>
/// <param name="regex">The regex object.</param>
public CallbackRegexFilter(Regex regex)
: base(clb => clb.Data, regex) { }
}
}
@@ -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,205 @@
using System.Text.RegularExpressions;
using Telegram.Bot.Types;
using Telegrator.Filters.Components;
using Telegrator.Handlers;
namespace Telegrator.Filters
{
/// <summary>
/// Abstract base class for filters that operate on command arguments.
/// Provides functionality to extract and validate command arguments from message text.
/// </summary>
/// <param name="index">The index of the argument to filter (0-based).</param>
public abstract class CommandArgumentFilterBase(int index) : Filter<Message>
{
/// <summary>
/// Gets the chosen argument at the specified index.
/// </summary>
protected string Target { get; private set; } = null!;
/// <summary>
/// Determines whether the filter can pass by extracting the command argument and validating it.
/// </summary>
/// <param name="context">The filter execution context containing the message.</param>
/// <returns>True if the argument exists and passes validation; otherwise, false.</returns>
public override bool CanPass(FilterExecutionContext<Message> context)
{
CommandHandlerAttribute attr = context.CompletedFilters.Get<CommandHandlerAttribute>(0);
string[] args = attr.Arguments ??= context.Input.SplitArgs();
Target = args.ElementAtOrDefault(index);
if (Target == null)
return false;
return CanPassNext(context);
}
/// <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>
protected abstract bool CanPassNext(FilterExecutionContext<Message> context);
}
/// <summary>
/// Filter that checks if a command has arguments count >= <paramref name="count"/>.
/// </summary>
/// <param name="count"></param>
public class ArgumentCountFilter(int count) : Filter<Message>
{
private readonly int Count = count;
/// <inheritdoc/>
public override bool CanPass(FilterExecutionContext<Message> context)
{
CommandHandlerAttribute attr = context.CompletedFilters.Get<CommandHandlerAttribute>(0);
string[] args = attr.Arguments ??= context.Input.SplitArgs();
return args.Length >= Count;
}
}
/// <summary>
/// Filter that checks if a command argument starts with a specified content.
/// </summary>
/// <param name="content">The content to check if the argument starts with.</param>
/// <param name="comparison">The string comparison type to use for the check.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentStartsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : CommandArgumentFilterBase(index)
{
/// <summary>
/// The content to check if the argument 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 command argument starts with the specified content using the configured comparison.
/// </summary>
/// <param name="_">The filter execution context (unused).</param>
/// <returns>True if the argument starts with the specified content; otherwise, false.</returns>
protected override bool CanPassNext(FilterExecutionContext<Message> _)
=> Target.StartsWith(Content, Comparison);
}
/// <summary>
/// Filter that checks if a command argument ends with a specified content.
/// </summary>
/// <param name="content">The content to check if the argument ends with.</param>
/// <param name="comparison">The string comparison type to use for the check.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentEndsWithFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : CommandArgumentFilterBase(index)
{
/// <summary>
/// The content to check if the argument 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 command argument ends with the specified content using the configured comparison.
/// </summary>
/// <param name="_">The filter execution context (unused).</param>
/// <returns>True if the argument ends with the specified content; otherwise, false.</returns>
protected override bool CanPassNext(FilterExecutionContext<Message> _)
=> Target.EndsWith(Content, Comparison);
}
/// <summary>
/// Filter that checks if a command argument contains a specified content.
/// </summary>
/// <param name="content">The content to check if the argument contains.</param>
/// <param name="comparison">The string comparison type to use for the check.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentContainsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : CommandArgumentFilterBase(index)
{
/// <summary>
/// The content to check if the argument 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 command argument contains the specified content using the configured comparison.
/// </summary>
/// <param name="_">The filter execution context (unused).</param>
/// <returns>True if the argument contains the specified content; otherwise, false.</returns>
protected override bool CanPassNext(FilterExecutionContext<Message> _)
=> Target.IndexOf(Content, Comparison) >= 0;
}
/// <summary>
/// Filter that checks if a command argument equals a specified content.
/// </summary>
/// <param name="content">The content to check if the argument equals.</param>
/// <param name="comparison">The string comparison type to use for the check.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentEqualsFilter(string content, StringComparison comparison = StringComparison.InvariantCulture, int index = 0) : CommandArgumentFilterBase(index)
{
/// <summary>
/// The content to check if the argument 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 command argument equals the specified content using the configured comparison.
/// </summary>
/// <param name="_">The filter execution context (unused).</param>
/// <returns>True if the argument equals the specified content; otherwise, false.</returns>
protected override bool CanPassNext(FilterExecutionContext<Message> _)
=> Target.Equals(Content, Comparison);
}
/// <summary>
/// Filter that checks if a command argument matches a regular expression pattern.
/// </summary>
/// <param name="regex">The regular expression to match against the argument.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public class ArgumentRegexFilter(Regex regex, int index = 0) : CommandArgumentFilterBase(index)
{
private readonly Regex _regex = regex;
/// <summary>
/// Gets the match found by the regex.
/// </summary>
public Match Match { get; private set; } = null!;
/// <summary>
/// Initializes a new instance of <see cref="ArgumentRegexFilter"/> with a regex pattern.
/// </summary>
/// <param name="pattern">The regular expression pattern to match.</param>
/// <param name="options">The regex options to use.</param>
/// <param name="matchTimeout">The timeout for the regex match operation.</param>
/// <param name="index">The index of the argument to check (0-based).</param>
public ArgumentRegexFilter(string pattern, RegexOptions options = RegexOptions.None, TimeSpan matchTimeout = default, int index = 0)
: this(new Regex(pattern, options, matchTimeout), index) { }
/// <summary>
/// Checks if the command argument matches the regular expression pattern.
/// </summary>
/// <param name="context">The filter execution context.</param>
/// <returns>True if the argument matches the regex pattern; otherwise, false.</returns>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
{
Match = _regex.Match(Target);
return Match.Success;
}
}
}
@@ -0,0 +1,108 @@
using Telegram.Bot.Types;
using Telegrator.Logging;
namespace Telegrator.Filters.Components
{
/// <summary>
/// Represents a compiled filter that applies a set of filters to an anonymous target type.
/// </summary>
public class AnonymousCompiledFilter : Filter<Update>, INamedFilter
{
private readonly Func<FilterExecutionContext<Update>, object, bool> FilterAction;
private readonly Func<Update, object?> GetFilterringTarget;
private readonly string _name;
/// <summary>
/// Gets the name of this compiled filter.
/// </summary>
public virtual string Name => _name;
/// <summary>
/// Initializes a new instance of the <see cref="AnonymousCompiledFilter"/> class.
/// </summary>
/// <param name="name">The name of the compiled filter.</param>
/// <param name="filterAction">The filter action delegate.</param>
/// <param name="getFilterringTarget">The function to get the filtering target from an update.</param>
private AnonymousCompiledFilter(string name, Func<Update, object?> getFilterringTarget, Func<FilterExecutionContext<Update>, object, bool> filterAction)
{
FilterAction = filterAction;
GetFilterringTarget = getFilterringTarget;
_name = name;
}
/// <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>(IEnumerable<IFilter<T>> filters, Func<Update, object?> getFilterringTarget) where T : class
{
return new AnonymousCompiledFilter(
string.Join("+", filters.Select(fltr => fltr.GetType().Name)),
getFilterringTarget,
(context, filterringTarget) => CanPassInternal(context, filters, filterringTarget));
}
/// <summary>
/// Compiles a set of filters into an <see cref="AnonymousCompiledFilter"/> for a specific target type with a custom name.
/// </summary>
/// <typeparam name="T">The type of the filtering target.</typeparam>
/// <param name="name">The custom name for the compiled filter.</param>
/// <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>(string name, IEnumerable<IFilter<T>> filters, Func<Update, object?> getFilterringTarget) where T : class
{
return new AnonymousCompiledFilter(
name,
getFilterringTarget,
(context, filterringTarget) => CanPassInternal(context, filters, filterringTarget));
}
/// <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>(FilterExecutionContext<Update> updateContext, IEnumerable<IFilter<T>> filters, object filterringTarget) where T : class
{
FilterExecutionContext<T> context = updateContext.CreateChild((T)filterringTarget);
foreach (IFilter<T> filter in filters)
{
if (!filter.CanPass(context))
{
if (filter is not AnonymousCompiledFilter && filter is not AnonymousTypeFilter)
Alligator.LogDebug("{0} filter of {1} didnt pass! (Compiled anonymous)", filter.GetType().Name, context.Data["handler_name"]);
return false;
}
context.CompletedFilters.Add(filter);
}
return true;
}
/// <inheritdoc/>
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,109 @@
using Telegram.Bot.Types;
using Telegrator.Logging;
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>, INamedFilter
{
private static readonly Type[] IgnoreLog = [typeof(CompiledFilter<>), typeof(AnonymousCompiledFilter), typeof(AnonymousTypeFilter)];
private readonly Func<FilterExecutionContext<Update>, object, bool> FilterAction;
private readonly Func<Update, object?> GetFilterringTarget;
private readonly string _name;
/// <summary>
/// Gets the name of this filter.
/// </summary>
public virtual string Name => _name;
/// <summary>
/// Initializes a new instance of the <see cref="AnonymousTypeFilter"/> class.
/// </summary>
/// <param name="name">The name of the filter.</param>
/// <param name="filterAction">The filter action delegate.</param>
/// <param name="getFilterringTarget">The function to get the filtering target from an update.</param>
public AnonymousTypeFilter(string name, Func<Update, object?> getFilterringTarget, Func<FilterExecutionContext<Update>, object, bool> filterAction)
{
FilterAction = filterAction;
GetFilterringTarget = getFilterringTarget;
_name = name;
}
/// <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(
filter.GetType().Name, getFilterringTarget,
(context, filterringTarget) => CanPassInternal(context, filter, filterringTarget));
}
/// <summary>
/// Compiles a filter for a specific target type with a custom name.
/// </summary>
/// <typeparam name="T">The type of the filtering target.</typeparam>
/// <param name="name">The custom name for the compiled filter.</param>
/// <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>(string name, IFilter<T> filter, Func<Update, T?> getFilterringTarget) where T : class
{
return new AnonymousTypeFilter(
name,
getFilterringTarget,
(context, filterringTarget) => CanPassInternal(context, filter, filterringTarget));
}
/// <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))
{
if (IgnoreLog.Contains(filter.GetType().MakeGenericType()))
Alligator.LogDebug("{0} filter of {1} didnt pass!", filter.GetType().Name, context.Data["handler_name"]);
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,63 @@
using Telegrator.Logging;
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>, INamedFilter where T : class
{
private readonly IFilter<T>[] Filters;
private readonly string _name;
/// <summary>
/// Gets the name of this compiled filter.
/// </summary>
public virtual string Name => _name;
/// <summary>
/// Initializes a new instance of the <see cref="CompiledFilter{T}"/> class.
/// </summary>
/// <param name="filters">The filters to compose.</param>
public CompiledFilter(params IFilter<T>[] filters)
{
_name = string.Join("+", filters.Select(fltr => fltr.GetType().Name));
Filters = filters;
}
/// <summary>
/// Initializes a new instance of the <see cref="CompiledFilter{T}"/> class with a custom name.
/// </summary>
/// <param name="name">The custom name for the compiled filter.</param>
/// <param name="filters">The filters to compose.</param>
public CompiledFilter(string name, params IFilter<T>[] filters)
{
_name = name;
Filters = 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))
{
if (filter is not AnonymousCompiledFilter && filter is not AnonymousTypeFilter)
Alligator.LogTrace("{0} filter of {1} didnt pass! (Compiled)", filter.GetType().Name, context.Data["handler_name"]);
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.OfType<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,39 @@
namespace Telegrator.Filters.Components
{
/// <summary>
/// Interface for filters that have a name for identification and debugging purposes.
/// </summary>
public interface INamedFilter
{
/// <summary>
/// Gets the name of the filter.
/// </summary>
public string Name { get; }
}
/// <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)
return _value == null;
if (_value == "{NOT_NULL}")
return true;
return envValue.Equals(_value, _comparison);
}
}
}
+135
View File
@@ -0,0 +1,135 @@
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 inverts the result of this filter.
/// </summary>
/// <typeparam name="Q"></typeparam>
/// <returns>A <see cref="ReverseFilter{T}"/> instance.</returns>
public static Filter<Q> Not<Q>(IFilter<Q> filter) where Q : class
=> new ReverseFilter<Q>(filter);
/// <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>
/// Creates a filter that passes if either this or the specified filter pass.
/// </summary>
/// <typeparam name="Q"></typeparam>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns>An <see cref="OrFilter{Q}"/> instance.</returns>
public static OrFilter<Q> Or<Q>(IFilter<Q> left, IFilter<Q> right) where Q : class
=> new OrFilter<Q>(left, right);
/// <summary>
/// Gets a value indicating whether this filter is collectible.
/// </summary>
public bool IsCollectible => GetType().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>
/// Implicitly creates <see cref="IFilter{T}"/> from function
/// </summary>
/// <param name="filter"></param>
public static implicit operator Filter<T>(Func<FilterExecutionContext<T>, bool> filter) => Filter<T>.If(filter);
}
/// <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);
}
}
+54
View File
@@ -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);
}
}
+64
View File
@@ -0,0 +1,64 @@
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
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 : MessageFilterBase
{
/// <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.TrimStart('@');
}
/// <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>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
{
if (Target.Text == null)
return false;
string userName = Mention ?? context.BotInfo.User.Username ?? throw new ArgumentNullException(nameof(context), "MentionedFilter requires BotInfo to be initialized");
IEnumerable<MessageEntity> entities = context.CompletedFilters
.Get<MessageHasEntityFilter>()
.SelectMany(ent => ent.FoundEntities)
.Where(ent => ent.Type == MessageEntityType.Mention);
foreach (MessageEntity ent in entities)
{
string mention = Target.Text.Substring(ent.Offset + 1, ent.Length - 1);
if (mention == userName)
return true;
}
return false;
}
}
}
@@ -0,0 +1,220 @@
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups;
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 : MessageFilterBase
{
/// <summary>
/// Gets the chat of the message being processed.
/// </summary>
public Chat Chat { get; private set; } = null!;
/// <inheritdoc/>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
{
Chat = Target.Chat;
return CanPassNext(context.CreateChild(Chat));
}
/// <summary>
/// Determines whether the filter passes for the given chat context.
/// </summary>
/// <param name="context">The filter execution context for the chat.</param>
/// <returns>True if the filter passes; otherwise, false.</returns>
protected abstract bool CanPassNext(FilterExecutionContext<Chat> context);
}
/// <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 : MessageChatFilter
{
private readonly ChatType? Type;
private readonly ChatTypeFlags? Flags;
/// <summary>
/// Initialize new instance of <see cref="MessageChatTypeFilter"/>
/// </summary>
/// <param name="type"></param>
public MessageChatTypeFilter(ChatType type)
=> Type = type;
/// <summary>
/// Initialize new instance of <see cref="MessageChatTypeFilter"/> with <see cref="ChatTypeFlags"/>
/// </summary>
/// <param name="type"></param>
public MessageChatTypeFilter(ChatTypeFlags type)
=> Flags = type;
/// <inheritdoc/>
protected override bool CanPassNext(FilterExecutionContext<Chat> _)
{
if (Type.HasValue)
return Chat.Type == Type.Value;
if (Flags != null)
{
ChatTypeFlags? asFlag = ToFlag(Chat.Type);
return asFlag.HasValue && Flags.Value.HasFlag(asFlag.Value);
}
return false;
}
private static ChatTypeFlags? ToFlag(ChatType type) => type switch
{
ChatType.Channel => ChatTypeFlags.Channel,
ChatType.Group => ChatTypeFlags.Group,
ChatType.Supergroup => ChatTypeFlags.Supergroup,
ChatType.Sender => ChatTypeFlags.Sender,
ChatType.Private => ChatTypeFlags.Private,
_ => null
};
}
/// <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;
}
}
}
+275
View File
@@ -0,0 +1,275 @@
using System.Text.RegularExpressions;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegrator.Filters.Components;
namespace Telegrator.Filters
{
/// <summary>
/// Base abstract class for all filter of <see cref="Message"/> updates
/// </summary>
public abstract class MessageFilterBase : Filter<Message>
{
/// <summary>
/// Target message for filterring
/// </summary>
protected Message Target { get; private set; } = null!;
/// <inheritdoc/>
protected virtual bool CanPassBase(FilterExecutionContext<Message> context)
{
FromReplyChainFilter? repliedFilter = context.CompletedFilters.Get<FromReplyChainFilter>().SingleOrDefault();
Target = repliedFilter?.Reply ?? context.Input;
if (Target is not { Id: > 0 })
return false;
return true;
}
/// <inheritdoc/>
public override bool CanPass(FilterExecutionContext<Message> context)
{
if (!CanPassBase(context))
return false;
return CanPassNext(context);
}
/// <summary>
/// Determines whether the filter can pass for the given context.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
protected abstract bool CanPassNext(FilterExecutionContext<Message> context);
}
/// <summary>
/// Filters messages by their <see cref="MessageType"/>.
/// </summary>
public class MessageTypeFilter : MessageFilterBase
{
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/>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
=> Target.Type == type;
}
/// <summary>
/// Filters messages that are automatic forwards.
/// </summary>
public class IsAutomaticFormwardMessageFilter : MessageFilterBase
{
/// <inheritdoc/>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
=> Target.IsAutomaticForward;
}
/// <summary>
/// Filters messages that are sent from offline.
/// </summary>
public class IsFromOfflineMessageFilter : MessageFilterBase
{
/// <inheritdoc/>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
=> Target.IsFromOffline;
}
/// <summary>
/// Filters service messages (e.g., chat events).
/// </summary>
public class IsServiceMessageMessageFilter : MessageFilterBase
{
/// <inheritdoc/>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
=> Target.IsServiceMessage;
}
/// <summary>
/// Filters messages that are topic messages.
/// </summary>
public class IsTopicMessageMessageFilter : MessageFilterBase
{
/// <inheritdoc/>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
=> Target.IsTopicMessage;
}
/// <summary>
/// Filters messages by dice throw value and optionally by dice type.
/// </summary>
public class DiceThrowedFilter : MessageFilterBase
{
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/>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
{
if (Target.Dice == null)
return false;
if (Target.Dice.Emoji != GetEmojyForDiceType(Dice))
return false;
return Target.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 : MessageFilterBase
{
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/>
protected override bool CanPassNext(FilterExecutionContext<Message> context)
{
if (context.Input is not { Entities.Length: > 0 })
return false;
FoundEntities = Target.Entities.Where(entity => FilterEntity(Target.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,102 @@
using Telegram.Bot.Types;
using Telegrator.Filters.Components;
namespace Telegrator.Filters
{
/// <summary>
/// Filter that checks if message has appropriate reply chain.
/// DOES NOT SHiFT MESSAGE FILTERS TARGET
/// </summary>
/// <param name="replyDepth">The depth of reply chain to traverse (default: 1).</param>
public class MessageHasReplyFilter(int replyDepth = 1) : Filter<Message>
{
/// <summary>
/// Gets the replied message at the specified depth in the reply chain.
/// </summary>
public Message Reply { get; private set; } = null!;
/// <summary>
/// Gets the depth of reply chain traversal.
/// </summary>
public int ReplyDepth { get; private set; } = replyDepth;
/// <summary>
/// Determines if the message can pass through the filter by first validating
/// the reply chain and then applying specific filter logic.
/// </summary>
/// <param name="context">The filter execution context containing the message.</param>
/// <returns>True if the message passes both reply validation and specific filter criteria; otherwise, false.</returns>
public override bool CanPass(FilterExecutionContext<Message> context)
{
Message reply = context.Input;
for (int i = ReplyDepth; i > 0; i--)
{
if (reply.ReplyToMessage is not { Id: > 0 } replyMessage)
return false;
reply = replyMessage;
}
Reply = reply;
return true;
}
}
/// <summary>
/// Helper filter class for filters that operate on replied messages.
/// Provides functionality to traverse reply chains and access replied message content
/// and shifts any next message filter to filter the content of found reply.
/// </summary>
/// <param name="replyDepth"></param>
public class FromReplyChainFilter(int replyDepth = 1) : Filter<Message>
{
/// <summary>
/// Gets the replied message at the specified depth in the reply chain.
/// </summary>
public Message Reply { get; private set; } = null!;
/// <summary>
/// Gets the depth of reply chain traversal.
/// </summary>
public int ReplyDepth { get; private set; } = replyDepth;
/// <summary>
/// Determines if the message can pass through the filter by first validating
/// the reply chain and then applying specific filter logic.
/// </summary>
/// <param name="context">The filter execution context containing the message.</param>
/// <returns>True if the message passes both reply validation and specific filter criteria; otherwise, false.</returns>
public override bool CanPass(FilterExecutionContext<Message> context)
{
Message reply = context.Input;
for (int i = ReplyDepth; i > 0; i--)
{
if (reply.ReplyToMessage is not { Id: > 0 } replyMessage)
return false;
reply = replyMessage;
}
Reply = reply;
return true;
}
}
/// <summary>
/// Filter that checks if the replied message was sent by the bot itself.
/// <para>( ! ): REQUIRES <see cref="MessageHasReplyFilter"/> before</para>
/// </summary>
public class MeRepliedFilter : Filter<Message>
{
/// <summary>
/// Checks if the replied message was sent by the bot.
/// </summary>
/// <param name="context">The filter execution context containing bot information.</param>
/// <returns>True if the replied message was sent by the bot; otherwise, false.</returns>
public override bool CanPass(FilterExecutionContext<Message> context)
{
MessageHasReplyFilter repliedFilter = context.CompletedFilters.Get<MessageHasReplyFilter>(0);
return context.BotInfo.User == repliedFilter.Reply.From;
}
}
}

Some files were not shown because too many files have changed in this diff Show More