├── .github ├── FUNDING.yml └── workflows │ ├── dotnet.yml │ └── release.yml ├── .gitignore ├── Deployf.Botf.sln ├── Deployf.Botf ├── ArgumentBinding │ ├── ArgumentAttributeBindState.cs │ ├── ArgumentBindBoolean.cs │ ├── ArgumentBindBridge.cs │ ├── ArgumentBindDateTime.cs │ ├── ArgumentBindEnum.cs │ ├── ArgumentBindGuid.cs │ ├── ArgumentBindInt32.cs │ ├── ArgumentBindInt64.cs │ ├── ArgumentBindSingle.cs │ ├── ArgumentBindString.cs │ ├── ArgumentBinder.cs │ └── IArgumentBind.cs ├── Attributes │ ├── ActionAttribute.cs │ ├── AllowAnonymousAttribute.cs │ ├── AuthorizeAttribute.cs │ ├── FilterAttribute.cs │ ├── Handle.cs │ ├── OnAttribute.cs │ └── StateAttribute.cs ├── BotController.cs ├── BotControllerState.cs ├── BotfProgram.cs ├── Deployf.Botf.csproj ├── Messages │ ├── MessageBuilder.cs │ └── MessageSender.cs ├── Middlewares │ ├── BotControllersAuthMiddleware.cs │ ├── BotControllersBeforeAllMiddleware.cs │ ├── BotControllersChainMiddleware.cs │ ├── BotControllersExceptionMiddleware.cs │ ├── BotControllersFSMMiddleware.cs │ ├── BotControllersInvokeMiddleware.cs │ ├── BotControllersMiddleware.cs │ └── BotControllersUnknownMiddleware.cs ├── Plugins │ ├── Calendar │ │ ├── CalendarMessageBuilder.cs │ │ └── CalendarState.cs │ └── FlagMessageBuilder.cs ├── Properties │ └── launchSettings.json ├── StartupExtensions.cs ├── System │ ├── BotContextAccessor.cs │ ├── BotControllerFactory.cs │ ├── BotControllerRoutes.cs │ ├── BotControllerStateService.cs │ ├── BotControllersInvoker.cs │ ├── BotRoutesExtensions.cs │ ├── BotServiceProvider.cs │ ├── BotfBot.cs │ ├── BotfException.cs │ ├── BotfOptions.cs │ ├── ChainStorage.cs │ ├── ChainTimeoutException.cs │ ├── ConnectionString.cs │ ├── CustomUpdatePollingManager.cs │ ├── Filters.cs │ ├── GlobalStateService.cs │ ├── IBotContextAccessor.cs │ ├── IGlobalStateService.cs │ ├── IKeyGenerator.cs │ ├── IKeyValueStorage.cs │ ├── InMemoryKeyValueStorage.cs │ ├── NumberExtensions.cs │ ├── Paging │ │ ├── PageFilter.cs │ │ ├── Paging.cs │ │ └── PagingService.cs │ ├── RouteStateSkipFunction.cs │ ├── StringExtensions.cs │ ├── TimeSpanFormatExtensions.cs │ ├── UpdateContextExtensions.cs │ └── UpdateMessageStrategies │ │ ├── EditTextMessageStrategy.cs │ │ ├── IUpdateMessageStrategy.cs │ │ ├── IUpdateMessageStrategyFactory.cs │ │ ├── MediaToMediaFileStrategy.cs │ │ ├── MediaToPlainTextStrategy.cs │ │ ├── PlainTextToMediaStrategy.cs │ │ └── UpdateMessageContext.cs ├── Telegram.Bot.Framework │ ├── ASP.NET Core │ │ └── TelegramBotMiddleware.cs │ ├── Abstractions │ │ ├── IBot.cs │ │ ├── IBotBuilder.cs │ │ ├── IBotOptions.cs │ │ ├── IBotServiceProvider.cs │ │ ├── IUpdateContext.cs │ │ ├── IUpdateHandler.cs │ │ ├── IUpdatePollingManager.cs │ │ └── UpdateDelegate.cs │ ├── BotBase.cs │ ├── BotOptions.cs │ ├── Update Pipeline │ │ └── BotBuilder.cs │ └── UpdateContext.cs ├── Users │ ├── BotUserService.cs │ ├── IBotUserService.cs │ └── UserClaims.cs └── deployf_logo_sq_128.png ├── Examples ├── .gitignore ├── Deployf.Botf.ActionButtonsExample │ ├── Deployf.Botf.ActionButtonsExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.ActionsAndQueryExample │ ├── Deployf.Botf.ActionsAndQueryExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.CalendarKeyboardExample │ ├── Deployf.Botf.CalendarKeyboardExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.ChainedExample │ ├── Deployf.Botf.ChainedExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.ControllerStateExample │ ├── Deployf.Botf.ControllerStateExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.GlobalStateMachineExample │ ├── Deployf.Botf.GlobalStateMachineExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.HelloExample │ ├── Deployf.Botf.HelloExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.MediaExample │ ├── Deployf.Botf.MediaExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.PingExample │ ├── Deployf.Botf.PingExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.ReminderBot │ ├── Deployf.Botf.ReminderBot.csproj │ ├── MainController.cs │ ├── Models.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── ReminderController.cs │ ├── ReminderJob.cs │ ├── UserService.cs │ └── appsettings.json ├── Deployf.Botf.ScheduleExample │ ├── AdminController.cs │ ├── Deployf.Botf.ScheduleExample.csproj │ ├── MainController.cs │ ├── Models.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── SlotController.Adding.cs │ ├── SlotController.Admin.cs │ ├── SlotController.Filling.cs │ ├── SlotController.Views.cs │ ├── SlotController.cs │ ├── SlotService.cs │ ├── UserService.cs │ └── appsettings.json ├── Deployf.Botf.SendAndUpdateMessagesExample │ ├── Deployf.Botf.SendAndUpdateMessagesExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json ├── Deployf.Botf.UnknownHandlingExample │ ├── Deployf.Botf.UnknownHandlingExample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json └── Deployf.Botf.WebAppExample │ ├── Deployf.Botf.WebAppExample.csproj │ ├── Pages │ └── Index.cshtml │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ └── appsettings.json ├── LICENSE ├── README.md └── build └── GetBuildVersion.psm1 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: deployf 2 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | env: 15 | BUILD_CONFIG: 'Release' 16 | SOLUTION: 'Deployf.Botf.sln' 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Setup .NET 24 | uses: actions/setup-dotnet@v1 25 | with: 26 | dotnet-version: '6.0.x' 27 | include-prerelease: true 28 | 29 | - name: Setup NuGet 30 | uses: NuGet/setup-nuget@v1.0.5 31 | 32 | - name: Restore dependencies 33 | run: nuget restore $SOLUTION 34 | 35 | - name: Build 36 | run: dotnet build $SOLUTION --configuration $BUILD_CONFIG --no-restore 37 | 38 | - name: Run tests 39 | run: dotnet test /p:Configuration=$BUILD_CONFIG --no-restore --no-build --verbosity normal 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | create: 5 | branches: 6 | - release/** 7 | push: 8 | branches: 9 | - release/** 10 | 11 | jobs: 12 | build: 13 | 14 | env: 15 | BUILD_CONFIG: 'Release' 16 | SOLUTION: 'Deployf.Botf.sln' 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Setup .NET 24 | uses: actions/setup-dotnet@v1 25 | with: 26 | dotnet-version: 6.0.x 27 | include-prerelease: true 28 | 29 | - name: Get Build Version 30 | run: | 31 | Import-Module .\build\GetBuildVersion.psm1 32 | Write-Host $Env:GITHUB_REF 33 | $version = GetBuildVersion -VersionString $Env:GITHUB_REF 34 | echo "BUILD_VERSION=$version" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 35 | shell: pwsh 36 | 37 | - name: Setup NuGet 38 | uses: NuGet/setup-nuget@v1.0.5 39 | 40 | - name: Restore dependencies 41 | run: nuget restore $SOLUTION 42 | 43 | - name: Build 44 | run: dotnet build $SOLUTION --configuration $BUILD_CONFIG -p:Version=$BUILD_VERSION --no-restore 45 | 46 | - name: Run tests 47 | run: dotnet test /p:Configuration=$BUILD_CONFIG --no-restore --no-build --verbosity normal 48 | 49 | - name: Publish 50 | if: startsWith(github.ref, 'refs/heads/release') 51 | run: nuget push **\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/bin 2 | **/obj 3 | **/appsettings.*.json 4 | .vs 5 | .vscode 6 | **/**.csproj.user 7 | **/db.sqlite 8 | /Examples/Deployf.Botf.ReminderBot/Hangfire.db 9 | **/publish 10 | .DS_Store 11 | **/.DS_Store 12 | .idea/ 13 | -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentAttributeBindState.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentAttributeBindState : IArgumentBind 7 | { 8 | public bool CanDecode(ParameterInfo parameter, object argument) 9 | { 10 | return parameter.CustomAttributes.Any(x => x.AttributeType == typeof(StateAttribute)); 11 | } 12 | 13 | public bool CanEncode(ParameterInfo parameter, object argument) 14 | { 15 | return parameter.CustomAttributes.Any(x => x.AttributeType == typeof(StateAttribute)); 16 | } 17 | 18 | readonly IKeyValueStorage _store; 19 | 20 | public ArgumentAttributeBindState(IKeyValueStorage store) 21 | { 22 | _store = store; 23 | } 24 | 25 | public async ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext ctx) 26 | { 27 | var userId = ctx.GetSafeUserId()!.Value; 28 | var attribute = parameter.GetCustomAttribute()!; 29 | var stateKey = attribute.Name ?? parameter.ParameterType.Name; 30 | var state = await _store.Get(userId, stateKey, attribute.DefauleValue); 31 | return state!; 32 | } 33 | 34 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext ctx) 35 | { 36 | return "."; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBindBoolean.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentBindBoolean : IArgumentBind 7 | { 8 | public bool CanDecode(ParameterInfo parameter, object argument) 9 | { 10 | return parameter.ParameterType == typeof(bool); 11 | } 12 | 13 | public bool CanEncode(ParameterInfo parameter, object argument) 14 | { 15 | return parameter.ParameterType == typeof(bool); 16 | } 17 | 18 | public ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext _) 19 | { 20 | return new(argument.ToString()! == "1"); 21 | } 22 | 23 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext _) 24 | { 25 | if(argument is bool boolValue) 26 | { 27 | return boolValue ? "1" : "0"; 28 | } 29 | 30 | return argument.ToString()!; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBindBridge.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentBindBridge : IArgumentBind 7 | { 8 | readonly IKeyValueStorage _store; 9 | readonly IKeyGenerator _keyGenerator; 10 | 11 | public ArgumentBindBridge(IKeyValueStorage store, IKeyGenerator keyGenerator) 12 | { 13 | _store = store; 14 | _keyGenerator = keyGenerator; 15 | } 16 | 17 | public bool CanDecode(ParameterInfo parameter, object argument) 18 | { 19 | return !parameter.ParameterType.IsPrimitive; 20 | } 21 | 22 | public bool CanEncode(ParameterInfo parameter, object argument) 23 | { 24 | return !parameter.ParameterType.IsPrimitive; 25 | } 26 | 27 | public async ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext ctx) 28 | { 29 | var userId = ctx.GetSafeUserId()!.Value; 30 | var objectId = "$_bridge_" + argument.ToString()!.Base64(); 31 | var state = await _store.Get(userId, objectId, null); 32 | 33 | // TODO: Check types 34 | 35 | return state!; 36 | } 37 | 38 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext ctx) 39 | { 40 | // TODO: check types 41 | 42 | var userId = ctx.GetSafeUserId()!.Value; 43 | var id = _keyGenerator.GetLongId(); 44 | var objectId = "$_bridge_" + id; 45 | _store.Set(userId, objectId, argument); 46 | return id.Base64(); 47 | } 48 | } -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBindDateTime.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentBindDateTime : IArgumentBind 7 | { 8 | public bool CanDecode(ParameterInfo parameter, object argument) 9 | { 10 | return parameter.ParameterType == typeof(DateTime); 11 | } 12 | 13 | public bool CanEncode(ParameterInfo parameter, object argument) 14 | { 15 | return parameter.ParameterType == typeof(DateTime); 16 | } 17 | 18 | public ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext _) 19 | { 20 | if (argument is string str) 21 | { 22 | var binary = long.Parse(str); 23 | return new(DateTime.FromBinary(binary)); 24 | } 25 | return ValueTask.FromException(new NotImplementedException("not implemented convertion")); 26 | } 27 | 28 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext _) 29 | { 30 | if (argument is DateTime dt) 31 | { 32 | var binary = dt.ToBinary(); 33 | return binary.ToString(); 34 | } 35 | throw new NotImplementedException("not implemented convertion"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBindEnum.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentBindEnum : IArgumentBind 7 | { 8 | public bool CanDecode(ParameterInfo parameter, object argument) 9 | { 10 | return parameter.ParameterType.IsEnum; 11 | } 12 | 13 | public bool CanEncode(ParameterInfo parameter, object argument) 14 | { 15 | return parameter.ParameterType.IsEnum; 16 | } 17 | 18 | public ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext _) 19 | { 20 | var str = argument.ToString(); 21 | if (Enum.TryParse(parameter.ParameterType, str, out var result)) 22 | { 23 | return new(result!); 24 | } 25 | return ValueTask.FromException(new NotImplementedException("enum conversion for current data is not implemented")); 26 | } 27 | 28 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext _) 29 | { 30 | return Enum.Format(parameter.ParameterType, argument, "D"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBindGuid.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentBindGuid : IArgumentBind 7 | { 8 | public bool CanDecode(ParameterInfo parameter, object argument) 9 | { 10 | return parameter.ParameterType == typeof(Guid); 11 | } 12 | 13 | public bool CanEncode(ParameterInfo parameter, object argument) 14 | { 15 | return parameter.ParameterType == typeof(Guid); 16 | } 17 | 18 | public ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext _) 19 | { 20 | if (argument is string str) 21 | { 22 | return new(new Guid(Convert.FromBase64String(str.Replace('_', '/')))); 23 | } 24 | return ValueTask.FromException(new NotImplementedException("not implemented convertion")); 25 | } 26 | 27 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext _) 28 | { 29 | if (argument is Guid guid) 30 | { 31 | return Convert.ToBase64String(guid.ToByteArray()).Replace('/', '_'); 32 | } 33 | throw new NotImplementedException("not implemented convertion"); 34 | } 35 | } -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBindInt32.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentBindInt32 : IArgumentBind 7 | { 8 | public bool CanDecode(ParameterInfo parameter, object argument) 9 | { 10 | return parameter.ParameterType == typeof(int); 11 | } 12 | 13 | public bool CanEncode(ParameterInfo parameter, object argument) 14 | { 15 | return parameter.ParameterType == typeof(int); 16 | } 17 | 18 | public ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext _) 19 | { 20 | return new (int.Parse(argument.ToString()!)); 21 | } 22 | 23 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext _) 24 | { 25 | return argument.ToString()!; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBindInt64.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentBindInt64 : IArgumentBind 7 | { 8 | public bool CanDecode(ParameterInfo parameter, object argument) 9 | { 10 | return parameter.ParameterType == typeof(long); 11 | } 12 | 13 | public bool CanEncode(ParameterInfo parameter, object argument) 14 | { 15 | return parameter.ParameterType == typeof(long); 16 | } 17 | 18 | public ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext _) 19 | { 20 | return new (long.Parse(argument.ToString()!)); 21 | } 22 | 23 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext _) 24 | { 25 | return argument.ToString()!; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBindSingle.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentBindSingle : IArgumentBind 7 | { 8 | public bool CanDecode(ParameterInfo parameter, object argument) 9 | { 10 | return parameter.ParameterType == typeof(float); 11 | } 12 | 13 | public bool CanEncode(ParameterInfo parameter, object argument) 14 | { 15 | return parameter.ParameterType == typeof(float); 16 | } 17 | 18 | public ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext _) 19 | { 20 | return new (float.Parse(argument.ToString()!)); 21 | } 22 | 23 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext _) 24 | { 25 | return argument.ToString()!; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBindString.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ArgumentBindString : IArgumentBind 7 | { 8 | public bool CanDecode(ParameterInfo parameter, object argument) 9 | { 10 | return parameter.ParameterType == typeof(string); 11 | } 12 | 13 | public bool CanEncode(ParameterInfo parameter, object argument) 14 | { 15 | return parameter.ParameterType == typeof(string); 16 | } 17 | 18 | public ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext _) 19 | { 20 | return new (argument); 21 | } 22 | 23 | public string Encode(ParameterInfo parameter, object argument, IUpdateContext _) 24 | { 25 | return argument.ToString()!; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/ArgumentBinder.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | public class ArgumentBinder 6 | { 7 | readonly IEnumerable _typeBinds; 8 | 9 | public ArgumentBinder(IEnumerable typeBinds) 10 | { 11 | _typeBinds = typeBinds; 12 | } 13 | 14 | public async ValueTask Bind(MethodInfo method, object[] args, IUpdateContext ctx) 15 | { 16 | List bindings = new(); 17 | var parameters = method.GetParameters(); 18 | for (int i = 0; i < parameters.Length; i++) 19 | { 20 | var p = parameters[i]; 21 | var arg = args[i]; 22 | var found = false; 23 | foreach (var binder in _typeBinds) 24 | { 25 | if (binder.CanDecode(p, arg)) 26 | { 27 | bindings.Add(await binder.Decode(p, arg, ctx)); 28 | found = true; 29 | break; 30 | } 31 | } 32 | 33 | if(found) 34 | { 35 | continue; 36 | } 37 | 38 | if(p.ParameterType.IsAssignableFrom(arg.GetType())) 39 | { 40 | bindings.Add(arg); 41 | continue; 42 | } 43 | 44 | throw new NotImplementedException($"Binding for parameter {p} for action {method} not found"); 45 | } 46 | return bindings.ToArray(); 47 | } 48 | 49 | public object[] Convert(MethodInfo method, object[] args, IUpdateContext ctx) 50 | { 51 | List bindings = new(); 52 | var parameters = method.GetParameters(); 53 | for (int i = 0; i < parameters.Length; i++) 54 | { 55 | var p = parameters[i]; 56 | var arg = args[i]; 57 | var found = false; 58 | foreach (var binder in _typeBinds) 59 | { 60 | if (binder.CanEncode(p, arg)) 61 | { 62 | bindings.Add(binder.Encode(p, arg, ctx)); 63 | found = true; 64 | break; 65 | } 66 | } 67 | 68 | if (!found) 69 | { 70 | throw new NotImplementedException($"Binding for parameter {p} for action {method} not found"); 71 | } 72 | } 73 | 74 | return bindings.ToArray(); 75 | } 76 | } -------------------------------------------------------------------------------- /Deployf.Botf/ArgumentBinding/IArgumentBind.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public interface IArgumentBind 7 | { 8 | bool CanDecode(ParameterInfo parameter, object argument); 9 | bool CanEncode(ParameterInfo parameter, object argument); 10 | 11 | string Encode(ParameterInfo parameter, object argument, IUpdateContext context); 12 | ValueTask Decode(ParameterInfo parameter, object argument, IUpdateContext context); 13 | } 14 | -------------------------------------------------------------------------------- /Deployf.Botf/Attributes/ActionAttribute.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Types.Enums; 2 | 3 | namespace Deployf.Botf; 4 | 5 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] 6 | public sealed class ActionAttribute : Attribute 7 | { 8 | public readonly string? Template; 9 | public readonly string? Desc; 10 | // public readonly UpdateType[] UpdateTypes; 11 | // public readonly MessageType[] MessageTypes; 12 | 13 | public ActionAttribute(string? template = null, string? desc = null) 14 | { 15 | Template = template; 16 | Desc = desc; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Deployf.Botf/Attributes/AllowAnonymousAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] 4 | public sealed class AllowAnonymousAttribute : Attribute 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /Deployf.Botf/Attributes/AuthorizeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] 4 | public sealed class AuthorizeAttribute : Attribute 5 | { 6 | public readonly string? Policy; 7 | 8 | public AuthorizeAttribute(string? policy = null) 9 | { 10 | Policy = policy; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Deployf.Botf/Attributes/FilterAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Deployf.Botf; 4 | 5 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] 6 | public sealed class FilterAttribute : Attribute 7 | { 8 | public readonly string Filter; 9 | public readonly BoolOp Operation; 10 | public readonly object? Param; 11 | 12 | public FilterAttribute(string? Or = null, string? And = null, string? OrNot = null, string? AndNot = null, string? Not = null, object? Param = null) 13 | { 14 | var arguments = new [] { Or, And, OrNot, AndNot, Not }.Where(c => c != null); 15 | if(arguments.Count() > 1 || !arguments.Any()) 16 | { 17 | throw new BotfException("You must pass only single argument into Filter() attribute"); 18 | } 19 | 20 | Filter = arguments.First()!; 21 | 22 | if(And != null) 23 | { 24 | Operation = BoolOp.And; 25 | } 26 | else if (Or != null) 27 | { 28 | Operation = BoolOp.Or; 29 | } 30 | else if (OrNot != null) 31 | { 32 | Operation = BoolOp.OrNot; 33 | } 34 | else if (AndNot != null) 35 | { 36 | Operation = BoolOp.AndNot; 37 | } 38 | else if (Not != null) 39 | { 40 | Operation = BoolOp.Not; 41 | } 42 | 43 | this.Param = Param; 44 | } 45 | 46 | public MethodInfo? GetMethod(Type? declaringType) => GetMethod(Filter, declaringType); 47 | 48 | public static MethodInfo? GetMethod(string filter, Type? declaringType) 49 | { 50 | if(filter.Contains('.')) 51 | { 52 | var typeName = filter.Substring(0, filter.LastIndexOf('.')); 53 | var methodName = filter.Substring(filter.LastIndexOf('.') + 1); 54 | 55 | var type = Type.GetType(typeName); 56 | if(type == null) 57 | { 58 | return null; 59 | } 60 | 61 | var method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 62 | return method; 63 | } 64 | 65 | return declaringType!.GetMethod(filter, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 66 | } 67 | 68 | public enum BoolOp 69 | { 70 | Not, 71 | And, 72 | Or, 73 | AndNot, 74 | OrNot, 75 | } 76 | } -------------------------------------------------------------------------------- /Deployf.Botf/Attributes/Handle.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public enum Handle 4 | { 5 | /// 6 | /// Means unknown command or message type from user (telegram) 7 | /// 8 | Unknown, 9 | 10 | /// 11 | /// User isn't authorized 12 | /// 13 | Unauthorized, 14 | 15 | /// 16 | /// Handle exception 17 | /// 18 | Exception, 19 | 20 | /// 21 | /// Execute action before message go to routing and whole the botf 22 | /// 23 | BeforeAll, 24 | ClearState, 25 | ChainTimeout 26 | } -------------------------------------------------------------------------------- /Deployf.Botf/Attributes/OnAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] 4 | public sealed class OnAttribute : Attribute 5 | { 6 | public readonly Handle Handler; 7 | public readonly string? Filter; 8 | public readonly int Order; 9 | 10 | public OnAttribute(Handle type) 11 | { 12 | Handler = type; 13 | Filter = null; 14 | Order = 0; 15 | } 16 | 17 | [Obsolete("Use Filter() attribute instead passing filter through On")] 18 | public OnAttribute(Handle type, string? filter) 19 | { 20 | Handler = type; 21 | Filter = filter; 22 | Order = 0; 23 | } 24 | 25 | [Obsolete("Use Filter() attribute instead passing filter through On")] 26 | public OnAttribute(Handle type, string? filter, int order) 27 | { 28 | Handler = type; 29 | Filter = filter; 30 | Order = order; 31 | } 32 | 33 | 34 | public OnAttribute(Handle type, int order) 35 | { 36 | Handler = type; 37 | Filter = null; 38 | Order = order; 39 | } 40 | } -------------------------------------------------------------------------------- /Deployf.Botf/Attributes/StateAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] 4 | public sealed class StateAttribute : Attribute 5 | { 6 | public readonly string? Name; 7 | public readonly object? DefauleValue; 8 | 9 | public StateAttribute(string? name = null, object? defauleValue = null) 10 | { 11 | Name = name; 12 | DefauleValue = defauleValue; 13 | } 14 | } -------------------------------------------------------------------------------- /Deployf.Botf/BotControllerState.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public static class Consts 4 | { 5 | public const string GLOBAL_STATE = "$gstate"; 6 | } 7 | 8 | public abstract class BotControllerState : BotController 9 | { 10 | public virtual ValueTask OnEnter() => ValueTask.CompletedTask; 11 | public virtual ValueTask OnLeave() => ValueTask.CompletedTask; 12 | } 13 | 14 | public abstract class BotControllerState : BotControllerState 15 | { 16 | public T? StateInstance { get; set; } 17 | 18 | public override async Task OnBeforeCall() 19 | { 20 | StateInstance = await Store!.Get(FromId, Consts.GLOBAL_STATE, default(T)); 21 | await base.OnBeforeCall(); 22 | } 23 | } -------------------------------------------------------------------------------- /Deployf.Botf/BotfProgram.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public class BotfProgram : BotController 4 | { 5 | public static void StartBot( 6 | string[] args, 7 | bool skipHello = false, 8 | Action? onConfigure = null, 9 | Action? onRun = null, 10 | BotfOptions? options = null) 11 | { 12 | if (!skipHello) 13 | { 14 | Console.ForegroundColor = ConsoleColor.Cyan; 15 | Console.WriteLine("==="); 16 | Console.WriteLine(" DEPLOY-F BotF"); 17 | Console.WriteLine(" Botf is a telegram bot framework with asp.net-like architecture"); 18 | Console.WriteLine(" For more information visit https://github.com/deploy-f/botf"); 19 | Console.WriteLine("==="); 20 | Console.WriteLine(""); 21 | Console.ResetColor(); 22 | } 23 | 24 | var builder = WebApplication.CreateBuilder(args); 25 | 26 | var botOptions = options; 27 | 28 | if(botOptions == null && builder.Configuration["bot"] != null) 29 | { 30 | var section = builder.Configuration.GetSection("bot"); 31 | botOptions = section.Get(); 32 | } 33 | 34 | var connectionString = builder.Configuration["botf"]; 35 | if (botOptions == null && connectionString != null) 36 | { 37 | botOptions = ConnectionString.Parse(connectionString); 38 | } 39 | 40 | if(botOptions == null) 41 | { 42 | throw new BotfException("Configuration is not passed. Check the appsettings*.json.\n" + 43 | "There must be configuration object like `{ \"bot\": { \"Token\": \"BotToken...\" } }`\n" + 44 | "Or connection string(in root) like `{ \"botf\": \"bot_token?key=value\" }`"); 45 | } 46 | 47 | builder.Services.AddBotf(botOptions); 48 | builder.Services.AddHttpClient(); 49 | 50 | onConfigure?.Invoke(builder.Services, builder.Configuration); 51 | 52 | var app = builder.Build(); 53 | app.UseBotf(); 54 | 55 | onRun?.Invoke(app, builder.Configuration); 56 | 57 | app.Run(); 58 | } 59 | 60 | public static void StartBot( 61 | string[] args, 62 | bool skipHello = false, 63 | Action? onConfigure = null, 64 | Action? onRun = null) where TBotService : class, IBotUserService 65 | { 66 | StartBot(args, skipHello, (svc, cfg) => 67 | { 68 | onConfigure?.Invoke(svc, cfg); 69 | svc.AddTransient(); 70 | }, onRun); 71 | } 72 | } -------------------------------------------------------------------------------- /Deployf.Botf/Deployf.Botf.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | Deploy-f, k0dep, wellon 8 | Deploy-f 9 | Make beautiful and clear telegram bots with the asp.net-like architecture 10 | Deploy-f 11 | https://github.com/deploy-f/botf 12 | deployf_logo_sq_128.png 13 | README.md 14 | https://github.com/deploy-f/botf 15 | telegram;bot;framework 16 | Library 17 | True 18 | true 19 | 20 | 21 | 22 | 23 | True 24 | \ 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Deployf.Botf/Messages/MessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Telegram.Bot.Types.Enums; 3 | using Telegram.Bot.Types.ReplyMarkups; 4 | 5 | namespace Deployf.Botf; 6 | 7 | public class MessageBuilder 8 | { 9 | public long ChatId { get; private set; } 10 | public StringBuilder BufferedMessage { get; private set; } = new StringBuilder(); 11 | public IReplyMarkup? Markup { get; set; } 12 | public string? PhotoUrl { get; set; } 13 | public List>? Reply { get; set; } 14 | public List>? Keyboard { get; set; } 15 | public int ReplyToMessageId { get; set; } = 0; 16 | public ParseMode ParseMode { get; set; } = ParseMode.Html; 17 | public bool IsDirty { get; set; } 18 | 19 | public string Message => BufferedMessage?.ToString() ?? string.Empty; 20 | 21 | public MessageBuilder SetChatId(long id) 22 | { 23 | ChatId = id; 24 | return this; 25 | } 26 | 27 | public MessageBuilder SetParseMode(ParseMode mode) 28 | { 29 | ParseMode = mode; 30 | return this; 31 | } 32 | 33 | public MessageBuilder SetMarkup(IReplyMarkup markup) 34 | { 35 | Markup = markup; 36 | IsDirty = true; 37 | return this; 38 | } 39 | 40 | public MessageBuilder SetPhotoUrl(string url) 41 | { 42 | PhotoUrl = url; 43 | IsDirty = true; 44 | return this; 45 | } 46 | 47 | public MessageBuilder PushL(string line = "") 48 | { 49 | Push(line + "\n"); 50 | return this; 51 | } 52 | 53 | public MessageBuilder Push(string line = "") 54 | { 55 | if (BufferedMessage == null) 56 | { 57 | BufferedMessage = new StringBuilder(); 58 | } 59 | 60 | BufferedMessage.Append(line); 61 | IsDirty = true; 62 | return this; 63 | } 64 | 65 | public MessageBuilder MakeButtonRow() 66 | { 67 | if (Reply == null) 68 | { 69 | Reply = new List>(); 70 | } 71 | 72 | Reply.Add(new List()); 73 | IsDirty = true; 74 | return this; 75 | } 76 | 77 | public MessageBuilder RowButton(string text, string payload) 78 | { 79 | MakeButtonRow(); 80 | Button(text, payload); 81 | return this; 82 | } 83 | 84 | public MessageBuilder LineButton(string text, string payload) 85 | { 86 | MakeButtonRow(); 87 | Button(text, payload); 88 | MakeButtonRow(); 89 | return this; 90 | } 91 | 92 | public MessageBuilder RowButton(InlineKeyboardButton button) 93 | { 94 | MakeButtonRow(); 95 | Button(button); 96 | return this; 97 | } 98 | 99 | public MessageBuilder Button(string text, string payload) 100 | { 101 | var button = payload.IsUrl() 102 | ? InlineKeyboardButton.WithUrl(text, payload) 103 | : InlineKeyboardButton.WithCallbackData(text, payload); 104 | 105 | Button(button); 106 | return this; 107 | } 108 | 109 | public MessageBuilder Button(InlineKeyboardButton button) 110 | { 111 | if (Reply == null) 112 | { 113 | MakeButtonRow(); 114 | } 115 | 116 | Reply!.Last().Add(button); 117 | Markup = new InlineKeyboardMarkup(Reply!.Where(c => c.Count > 0)); 118 | IsDirty = true; 119 | return this; 120 | } 121 | 122 | 123 | 124 | public MessageBuilder MakeKButtonRow() 125 | { 126 | if (Keyboard == null) 127 | { 128 | Keyboard = new List>(); 129 | } 130 | 131 | Keyboard.Add(new List()); 132 | IsDirty = true; 133 | return this; 134 | } 135 | 136 | public MessageBuilder RowKButton(string text) 137 | { 138 | MakeKButtonRow(); 139 | KButton(text); 140 | return this; 141 | } 142 | 143 | public MessageBuilder LineKButton(string text) 144 | { 145 | MakeKButtonRow(); 146 | KButton(text); 147 | MakeKButtonRow(); 148 | return this; 149 | } 150 | 151 | public MessageBuilder RowKButton(KeyboardButton button) 152 | { 153 | MakeKButtonRow(); 154 | KButton(button); 155 | return this; 156 | } 157 | 158 | public MessageBuilder KButton(string text) 159 | { 160 | KButton(new KeyboardButton(text)); 161 | return this; 162 | } 163 | 164 | public MessageBuilder KButton(KeyboardButton button) 165 | { 166 | if (Keyboard == null) 167 | { 168 | MakeKButtonRow(); 169 | } 170 | 171 | Keyboard!.Last().Add(button); 172 | Markup = new ReplyKeyboardMarkup(Keyboard!.Where(c => c.Count > 0)) { ResizeKeyboard = true }; 173 | IsDirty = true; 174 | return this; 175 | } 176 | 177 | public MessageBuilder RemoveKeyboard() 178 | { 179 | Keyboard = null; 180 | Markup = null; 181 | IsDirty = true; 182 | return this; 183 | } 184 | 185 | 186 | public MessageBuilder Pager(Paging page, Func row, string format, int buttonsInRow = 2) 187 | { 188 | var i = 0; 189 | foreach (var item in page.Items) 190 | { 191 | var r = row(item); 192 | if (i != 0 && (i % buttonsInRow) == 0) 193 | { 194 | RowButton(r.text, r.data); 195 | } 196 | else 197 | { 198 | Button((string)r.text, (string)r.data); 199 | } 200 | i++; 201 | } 202 | 203 | MakeButtonRow(); 204 | 205 | if (page.PageNumber > 0) 206 | { 207 | Button($"⬅️", string.Format(format, page.PageNumber - 1)); 208 | } 209 | 210 | if (page.PageNumber < ((page.Count / (float)page.ItemsPerPage) - 1)) 211 | { 212 | Button($"➡️", string.Format(format, page.PageNumber + 1)); 213 | } 214 | 215 | return this; 216 | } 217 | 218 | public MessageBuilder ReplyTo(int messageId = default) 219 | { 220 | ReplyToMessageId = messageId; 221 | return this; 222 | } 223 | } -------------------------------------------------------------------------------- /Deployf.Botf/Messages/MessageSender.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot; 2 | using Telegram.Bot.Types; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class MessageSender 7 | { 8 | readonly ITelegramBotClient _client; 9 | 10 | public MessageSender(ITelegramBotClient client) 11 | { 12 | _client = client; 13 | } 14 | 15 | // TODO: to catch api exceptions about "forbidden" 16 | public async ValueTask Send(MessageBuilder message, CancellationToken token = default) 17 | { 18 | if (message.PhotoUrl == null) 19 | { 20 | return await _client.SendTextMessageAsync( 21 | message.ChatId, 22 | message.Message, 23 | null, 24 | message.ParseMode, 25 | replyMarkup: message.Markup, 26 | cancellationToken: token, 27 | replyToMessageId: message.ReplyToMessageId 28 | ); 29 | } 30 | else 31 | { 32 | return await _client.SendPhotoAsync( 33 | message.ChatId, 34 | new InputFileUrl(message.PhotoUrl), 35 | null, 36 | message.Message, 37 | message.ParseMode, 38 | replyMarkup: message.Markup, 39 | cancellationToken: token, 40 | replyToMessageId: message.ReplyToMessageId 41 | ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Deployf.Botf/Middlewares/BotControllersAuthMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class BotControllersAuthMiddleware : IUpdateHandler 7 | { 8 | readonly ILogger _log; 9 | readonly BotUserService _tokenService; 10 | 11 | public BotControllersAuthMiddleware(ILogger log, BotUserService tokenService) 12 | { 13 | _log = log; 14 | _tokenService = tokenService; 15 | } 16 | 17 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 18 | { 19 | if(context.Items.TryGetValue("controller", out var value) && value is BotController controller) 20 | { 21 | var method = (MethodInfo)context.Items["action"]; 22 | var user = await _tokenService.GetUser(context.GetSafeUserId()); 23 | context.Items["user"] = user; 24 | controller.User = user; 25 | AuthMethod(controller, method); 26 | } 27 | await next(context, cancellationToken); 28 | } 29 | 30 | void AuthMethod(BotController controller, MethodInfo method) 31 | { 32 | var policy = method.GetAuthPolicy()?.Trim(); 33 | 34 | if(policy == null) 35 | { 36 | _log.LogDebug("Authorization skipping"); 37 | return; 38 | } 39 | 40 | if(policy == string.Empty && !controller.User.IsAuthorized) 41 | { 42 | throw new UnauthorizedAccessException(); 43 | } 44 | 45 | if (policy == "admin" && !controller.User.IsInRole("admin")) 46 | { 47 | throw new UnauthorizedAccessException("you are not admin"); 48 | } 49 | 50 | if(!string.IsNullOrEmpty(policy) && !controller.User.IsInRole(policy)) 51 | { 52 | throw new UnauthorizedAccessException(); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Deployf.Botf/Middlewares/BotControllersBeforeAllMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | using Telegram.Bot.Types.Enums; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class BotControllersBeforeAllMiddleware : IUpdateHandler 7 | { 8 | readonly BotControllersInvoker _invoker; 9 | readonly BotControllerHandlers _handlers; 10 | readonly BotfOptions _options; 11 | 12 | public BotControllersBeforeAllMiddleware(BotControllersInvoker invoker, BotControllerHandlers handlers, BotfOptions options) 13 | { 14 | _invoker = invoker; 15 | _handlers = handlers; 16 | _options = options; 17 | } 18 | 19 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 20 | { 21 | var update = context.Update; 22 | var message = context.Update.Message ?? context.Update.EditedMessage; 23 | // avoid handling updates in group that is not addressed to bot 24 | if (_options.HandleOnlyMentionedInGroups 25 | && (update.Type == UpdateType.EditedMessage || update.Type == UpdateType.Message) 26 | && message!.Chat.Id != message.From!.Id // detect that we are in private chat with user 27 | && !((message.ReplyToMessage != null && message.ReplyToMessage.From!.Username == _options.Username) 28 | || (message.Text!.Contains(_options.UsernameTag!))) 29 | ){ 30 | return; 31 | } 32 | 33 | var handlers = _handlers.TryFindHandlers(Handle.BeforeAll, context); 34 | foreach(var handler in handlers) 35 | { 36 | if(context.IsHandlingStopRequested()) 37 | { 38 | break; 39 | } 40 | await _invoker.Invoke(context, cancellationToken, handler); 41 | } 42 | 43 | await next(context, cancellationToken); 44 | } 45 | } -------------------------------------------------------------------------------- /Deployf.Botf/Middlewares/BotControllersChainMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public class BotControllersChainMiddleware : IUpdateHandler 6 | { 7 | readonly ILogger _log; 8 | readonly ChainStorage _chainStorage; 9 | readonly BotControllersInvoker _invoker; 10 | readonly BotControllerHandlers _handlers; 11 | 12 | public BotControllersChainMiddleware(ILogger log, ChainStorage chainStorage, BotControllersInvoker invoker, BotControllerHandlers handlers) 13 | { 14 | _log = log; 15 | _chainStorage = chainStorage; 16 | _invoker = invoker; 17 | _handlers = handlers; 18 | } 19 | 20 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 21 | { 22 | try 23 | { 24 | var id = context.GetSafeChatId(); 25 | if (id != null) 26 | { 27 | var chain = _chainStorage.Get(id.Value); 28 | if (chain != null) 29 | { 30 | _log.LogTrace("Found chain for user {userId}, triggered continue execution of chain", id.Value); 31 | _chainStorage.Clear(id.Value); 32 | if (chain.Synchronizator != null) 33 | { 34 | chain.Synchronizator.SetResult(context); 35 | } 36 | } 37 | else 38 | { 39 | await next(context, cancellationToken); 40 | } 41 | } 42 | else 43 | { 44 | await next(context, cancellationToken); 45 | } 46 | } 47 | catch(ChainTimeoutException e) 48 | { 49 | _log.LogDebug("Chain timeout reached for chat {chatId}", context.GetChatId()); 50 | if (!e.Handled) 51 | { 52 | var handlers = _handlers.TryFindHandlers(Handle.ChainTimeout, context); 53 | foreach(var handler in handlers) 54 | { 55 | if(context.IsHandlingStopRequested()) 56 | { 57 | break; 58 | } 59 | await _invoker.Invoke(context, cancellationToken, handler); 60 | } 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /Deployf.Botf/Middlewares/BotControllersExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public class BotControllersExceptionMiddleware : IUpdateHandler 6 | { 7 | readonly BotControllersInvoker _invoker; 8 | readonly BotControllerHandlers _handlers; 9 | readonly ILogger _logger; 10 | 11 | public BotControllersExceptionMiddleware(BotControllersInvoker invoker, BotControllerHandlers handlers, ILogger logger) 12 | { 13 | _invoker = invoker; 14 | _handlers = handlers; 15 | _logger = logger; 16 | } 17 | 18 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 19 | { 20 | try 21 | { 22 | await next(context, cancellationToken); 23 | } 24 | catch (UnauthorizedAccessException ex) 25 | { 26 | var handlers = _handlers.TryFindHandlers(Handle.Unauthorized, context).ToList(); 27 | 28 | if (handlers.Count > 0) 29 | { 30 | foreach(var handler in handlers) 31 | { 32 | if(context.IsHandlingStopRequested()) 33 | { 34 | break; 35 | } 36 | await _invoker.Invoke(context, cancellationToken, handler); 37 | } 38 | } 39 | else 40 | { 41 | await ProcessException(ex); 42 | } 43 | } 44 | catch (Exception ex) 45 | { 46 | await ProcessException(ex); 47 | } 48 | 49 | async Task ProcessException(Exception ex) 50 | { 51 | var handlers = _handlers.TryFindHandlers(Handle.Exception, context).ToList(); 52 | 53 | if (handlers.Count == 0) 54 | { 55 | _logger.LogError(ex, "unhandled exception"); 56 | return; 57 | } 58 | 59 | foreach(var handler in handlers) 60 | { 61 | if(context.IsHandlingStopRequested()) 62 | { 63 | break; 64 | } 65 | if (handler.GetParameters().Length == 0) 66 | { 67 | await _invoker.Invoke(context, cancellationToken, handler); 68 | } 69 | else 70 | { 71 | await _invoker.Invoke(context, cancellationToken, handler, ex); 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Deployf.Botf/Middlewares/BotControllersFSMMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public class BotControllersFSMMiddleware : IUpdateHandler 6 | { 7 | public const string STATE_KEY = "$store"; 8 | 9 | readonly ILogger _log; 10 | readonly BotControllerStates _map; 11 | readonly IKeyValueStorage _store; 12 | 13 | public BotControllersFSMMiddleware(ILogger log, BotControllerStates map, IKeyValueStorage store) 14 | { 15 | _log = log; 16 | _map = map; 17 | _store = store; 18 | } 19 | 20 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 21 | { 22 | Func? afterNext = null; 23 | 24 | var uid = context.GetSafeUserId(); 25 | if (!context.Items.ContainsKey("controller") && uid != null) 26 | { 27 | var state = await _store.Get(uid.Value, STATE_KEY, null); 28 | 29 | if (state != null) 30 | { 31 | afterNext = () => _store.Remove(uid.Value, STATE_KEY); 32 | } 33 | 34 | if (state != null && _map.TryGetValue(state.GetType(), out var value) && value != null) 35 | { 36 | _log.LogDebug("Found bot state handler {Controller}.{Method}, State: {State}", 37 | value.DeclaringType!.Name, 38 | value.Name, 39 | state); 40 | 41 | var controller = (BotController)context.Services.GetRequiredService(value.DeclaringType); 42 | controller.Init(context, cancellationToken); 43 | context.Items["args"] = new object[] { state }; 44 | context.Items["action"] = value; 45 | context.Items["controller"] = controller; 46 | context.Items["skip_binding_marker"] = true; 47 | 48 | afterNext = async () => 49 | { 50 | if (state == await _store.Get(uid.Value, STATE_KEY, null)) 51 | { 52 | await _store.Remove(uid.Value, STATE_KEY); 53 | } 54 | }; 55 | } 56 | } 57 | 58 | await next(context, cancellationToken); 59 | if(afterNext != null) 60 | { 61 | await afterNext(); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /Deployf.Botf/Middlewares/BotControllersInvokeMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public class BotControllersInvokeMiddleware : IUpdateHandler 6 | { 7 | readonly BotControllersInvoker _invoker; 8 | 9 | public BotControllersInvokeMiddleware(BotControllersInvoker invoker) 10 | { 11 | _invoker = invoker; 12 | } 13 | 14 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 15 | { 16 | var invoked = await _invoker.Invoke(context); 17 | if (!invoked) 18 | { 19 | await next(context, cancellationToken); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Deployf.Botf/Middlewares/BotControllersMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | using Telegram.Bot.Types.Enums; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class BotControllersMiddleware : IUpdateHandler 7 | { 8 | readonly ILogger _log; 9 | readonly BotControllerRoutes _map; 10 | readonly IBotContextAccessor _accessor; 11 | readonly BotfOptions _opts; 12 | 13 | public BotControllersMiddleware(BotControllerRoutes map, IBotContextAccessor accessor, ILogger log, BotfOptions opts) 14 | { 15 | _map = map; 16 | _accessor = accessor; 17 | _log = log; 18 | _opts = opts; 19 | } 20 | 21 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 22 | { 23 | _accessor.Context = context; 24 | 25 | var payload = context.GetSafeTextPayload(); 26 | if (!string.IsNullOrEmpty(payload)) 27 | { 28 | string[] entries; 29 | if (payload[0] == '/') 30 | { 31 | entries = payload.Split(" ", StringSplitOptions.RemoveEmptyEntries); 32 | } 33 | else 34 | { 35 | entries = payload.Split("/", StringSplitOptions.RemoveEmptyEntries); 36 | } 37 | 38 | var key = ExtractGroupKey(context, entries[0]); 39 | 40 | var arguments = entries.Skip(1).ToArray(); 41 | var value = await _map.GetValue(key, arguments, context); 42 | if (value != null) 43 | { 44 | _log.LogDebug("Found bot action {Controller}.{Method}. Payload: {Payload} Arguments: {@Args}", 45 | value.DeclaringType!.Name, 46 | value.Name, 47 | payload, 48 | arguments); 49 | 50 | var controller = (BotController)context.Services.GetRequiredService(value.DeclaringType); 51 | controller.Init(context, cancellationToken); 52 | context.Items["args"] = arguments; 53 | context.Items["action"] = value; 54 | context.Items["controller"] = controller; 55 | } 56 | } 57 | 58 | await next(context, cancellationToken); 59 | } 60 | 61 | private string ExtractGroupKey(IUpdateContext context, string key) 62 | { 63 | var updateType = context.Update.Type; 64 | var message = context.Update.Message ?? context.Update.EditedMessage; 65 | // detect commands in chats like "/command@botname ..." 66 | if (key[0] == '/' 67 | && (updateType == UpdateType.EditedMessage || updateType == UpdateType.Message) 68 | && message!.Chat.Id != message.From!.Id 69 | && key.Contains('@') 70 | && key.Count(CharEqualDog) == 1 71 | && key.EndsWith(_opts.UsernameTag!)) 72 | { 73 | var tuple = key.Split('@', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); 74 | key = tuple[0]; 75 | } 76 | 77 | return key; 78 | } 79 | 80 | static bool CharEqualDog(char c) => c == '@'; 81 | } -------------------------------------------------------------------------------- /Deployf.Botf/Middlewares/BotControllersUnknownMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public class BotControllersUnknownMiddleware : IUpdateHandler 6 | { 7 | readonly BotControllersInvoker _invoker; 8 | readonly BotControllerHandlers _handlers; 9 | 10 | public BotControllersUnknownMiddleware(BotControllersInvoker invoker, BotControllerHandlers handlers) 11 | { 12 | _invoker = invoker; 13 | _handlers = handlers; 14 | } 15 | 16 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 17 | { 18 | var handlers = _handlers.TryFindHandlers(Handle.Unknown, context); 19 | var processed = false; 20 | 21 | foreach(var handle in handlers) 22 | { 23 | if(context.IsHandlingStopRequested()) 24 | { 25 | break; 26 | } 27 | await _invoker.Invoke(context, cancellationToken, handle); 28 | processed = true; 29 | } 30 | 31 | if (!processed) 32 | { 33 | await next(context, cancellationToken); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Deployf.Botf/Plugins/FlagMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public class FlagMessageBuilder where T : struct, Enum 4 | { 5 | private readonly T _value; 6 | private Func? _navigation; 7 | 8 | public FlagMessageBuilder(T value) 9 | { 10 | _value = value; 11 | } 12 | 13 | public FlagMessageBuilder Navigation(Func nav) 14 | { 15 | _navigation = nav; 16 | return this; 17 | } 18 | 19 | public void Build(MessageBuilder b) 20 | { 21 | var flags = Enum.GetNames(); 22 | var values = Enum.GetValues(); 23 | 24 | for (int i = 0; i < values.Length; i++) 25 | { 26 | T value = values[i]; 27 | var isSet = _value.HasFlag(value); 28 | 29 | var title = value.ToString(); 30 | if (isSet) 31 | { 32 | title = "🔘" + title; 33 | } 34 | 35 | b.RowButton(title, _navigation!(Enum.Parse((Convert.ToInt32(_value) ^ Convert.ToInt32(value)).ToString()))); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Deployf.Botf/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:60861/", 7 | "sslPort": 44397 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Deployf.Botf": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/BotContextAccessor.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public class BotContextAccessor : IBotContextAccessor 6 | { 7 | public IUpdateContext? Context { get; set; } 8 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/BotControllerRoutes.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public record RouteInfo( 7 | MethodInfo Method, 8 | RouteSkipDelegate? Skip 9 | ); 10 | 11 | public abstract class BotControllerMap : Dictionary where T : notnull 12 | { 13 | public BotControllerMap(IDictionary data) : base(data) 14 | { 15 | } 16 | 17 | public IEnumerable ControllerTypes() 18 | { 19 | return Values 20 | .Select(c => c.DeclaringType!) 21 | .Distinct(); 22 | } 23 | } 24 | 25 | public abstract class BotControllerListMap : List<(T command, RouteInfo info)> where T : notnull 26 | { 27 | protected readonly ILookup> _lookup; 28 | 29 | public BotControllerListMap(IList<(T command, RouteInfo info)> data) : base(data) 30 | { 31 | _lookup = data.ToLookup(c => c.command, c => c.info); 32 | } 33 | 34 | public IEnumerable ControllerTypes() 35 | { 36 | return this 37 | .Select(c => c.info.Method.DeclaringType!) 38 | .Distinct(); 39 | } 40 | } 41 | 42 | public class BotControllerRoutes : BotControllerListMap 43 | { 44 | static readonly Type STATE_TYPE = typeof(BotControllerState); 45 | 46 | public BotControllerRoutes(IList<(string command, RouteInfo action)> data) :base(data) 47 | { 48 | } 49 | 50 | public (string? template, MethodInfo? method) FindTemplate(string controller, string action, object[] args) 51 | { 52 | foreach(var item in this) 53 | { 54 | if(item.info.Method.Name == action 55 | && item.info.Method.DeclaringType!.Name == controller 56 | && args.Length == item.info.Method.GetParameters().Length) //TODO: check the argument types 57 | { 58 | return (item.command, item.info.Method); 59 | } 60 | } 61 | 62 | return (null, null); 63 | } 64 | 65 | public async ValueTask GetValue(string key, string[] arguments, IUpdateContext context) 66 | { 67 | if(!_lookup.Contains(key)) 68 | { 69 | return null; 70 | } 71 | 72 | var targets = _lookup[key]; 73 | foreach (var item in targets) 74 | { 75 | if(item.Skip != null && await item.Skip(key, item, context)) 76 | { 77 | continue; 78 | } 79 | 80 | if (arguments.Length == item.Method.GetParameters().Length) //TODO: check the argument types 81 | { 82 | return item.Method; 83 | } 84 | } 85 | return null; 86 | } 87 | 88 | public Type? GetStateType(Type stateType) 89 | { 90 | return this.Where(c => STATE_TYPE.IsAssignableFrom(c.info.Method.DeclaringType)) 91 | .FirstOrDefault(c => c.info!.Method!.DeclaringType!.BaseType!.GenericTypeArguments[0] == stateType).info?.Method?.DeclaringType; 92 | } 93 | } 94 | 95 | public class BotControllerStates : BotControllerMap 96 | { 97 | public BotControllerStates(IDictionary data) : base(data) 98 | { 99 | } 100 | } 101 | 102 | public delegate bool ActionFilter(IUpdateContext ctx); 103 | 104 | public class HandlerItem 105 | { 106 | public Handle Handler { get; set; } 107 | public ActionFilter? Filter { get; set; } 108 | public MethodInfo TargetMethod { get; set; } 109 | 110 | public HandlerItem(Handle handler, MethodInfo targetMethod, ActionFilter? filter = null) 111 | { 112 | Handler = handler; 113 | Filter = filter; 114 | TargetMethod = targetMethod; 115 | } 116 | 117 | public bool TryFilter(IUpdateContext context) 118 | { 119 | if(Filter == null) 120 | { 121 | return true; 122 | } 123 | 124 | context.SetCurrentHandler(this); 125 | 126 | return Filter(context); 127 | } 128 | } 129 | 130 | public class BotControllerHandlers 131 | { 132 | readonly List Handlers; 133 | readonly Dictionary> LookupTable; 134 | 135 | public BotControllerHandlers(IEnumerable data) 136 | { 137 | Handlers = new List(data); 138 | LookupTable = BuildLookupTable(); 139 | } 140 | 141 | private Dictionary> BuildLookupTable() 142 | { 143 | var table = new Dictionary>(); 144 | foreach(var item in Handlers) 145 | { 146 | if(table.TryGetValue(item.Handler, out var lookup)) 147 | { 148 | lookup.Add(item); 149 | } 150 | else 151 | { 152 | lookup = new List(); 153 | lookup.Add(item); 154 | table[item.Handler] = lookup; 155 | } 156 | } 157 | 158 | return table; 159 | } 160 | 161 | public IEnumerable TryFindHandlers(Handle handle, IUpdateContext context) 162 | { 163 | foreach (var item in Handlers) 164 | { 165 | if(item.Handler != handle) 166 | { 167 | continue; 168 | } 169 | 170 | if(item.TryFilter(context)) 171 | { 172 | yield return item.TargetMethod; 173 | } 174 | } 175 | } 176 | 177 | public IReadOnlyList? GetHandlers(Handle handle) 178 | { 179 | if(LookupTable.TryGetValue(handle, out var lookup)) 180 | { 181 | return lookup; 182 | } 183 | 184 | return null; 185 | } 186 | 187 | public IEnumerable ControllerTypes() 188 | { 189 | return Handlers 190 | .Select(c => c.TargetMethod.DeclaringType!) 191 | .Distinct(); 192 | } 193 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/BotControllerStateService.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public struct BotControllerStateService 6 | { 7 | public static Dictionary?> _savers = new (); 8 | public static Dictionary?> _loaders = new (); 9 | 10 | public async Task Load(BotController controller) 11 | { 12 | var controllerType = controller.GetType(); 13 | if (_loaders.TryGetValue(controllerType, out var loader) && loader != null) 14 | { 15 | await loader(controller); 16 | return; 17 | } 18 | 19 | List fields; 20 | List props; 21 | ExtractMembers(controllerType, out fields, out props); 22 | 23 | if (fields.Count > 0 || props.Count > 0) 24 | { 25 | loader = async (BotController _controller) => 26 | { 27 | var storage = _controller.Store!; 28 | 29 | foreach (var field in fields) 30 | { 31 | var value = await storage.Get(_controller.FromId, GetKey(field, controllerType), null); 32 | field.SetValue(_controller, value); 33 | } 34 | 35 | foreach (var prop in props) 36 | { 37 | var value = await storage.Get(_controller.FromId, GetKey(prop, controllerType), null); 38 | prop.SetValue(_controller, value); 39 | } 40 | }; 41 | 42 | _loaders[controllerType] = loader; 43 | 44 | await loader(controller); 45 | } 46 | else 47 | { 48 | _loaders[controllerType] = null; 49 | } 50 | } 51 | 52 | public async Task Save(BotController controller) 53 | { 54 | var controllerType = controller.GetType(); 55 | if (_savers.TryGetValue(controllerType, out var saver) && saver != null) 56 | { 57 | await saver(controller); 58 | return; 59 | } 60 | 61 | List fields; 62 | List props; 63 | ExtractMembers(controllerType, out fields, out props); 64 | 65 | if (fields.Count > 0 || props.Count > 0) 66 | { 67 | saver = async (BotController _controller) => 68 | { 69 | var storage = _controller.Store!; 70 | 71 | foreach (var field in fields) 72 | { 73 | var value = field.GetValue(_controller); 74 | var key = GetKey(field, controllerType); 75 | if(value != null) 76 | { 77 | await storage.Set(_controller.FromId, key, value); 78 | } 79 | else 80 | { 81 | await storage.Remove(_controller.FromId, key); 82 | } 83 | } 84 | 85 | foreach (var prop in props) 86 | { 87 | var value = prop.GetValue(_controller); 88 | var key = GetKey(prop, controllerType); 89 | if(value != null) 90 | { 91 | await storage.Set(_controller.FromId, key, value); 92 | } 93 | else 94 | { 95 | await storage.Remove(_controller.FromId, key); 96 | } 97 | } 98 | }; 99 | 100 | _savers[controllerType] = saver; 101 | 102 | await saver(controller); 103 | } 104 | else 105 | { 106 | _savers[controllerType] = null; 107 | } 108 | } 109 | 110 | static void ExtractMembers(Type controllerType, out List fields, out List props) 111 | { 112 | const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy; 113 | 114 | fields = controllerType 115 | .GetFields(bindingFlags) 116 | .Where(c => c.GetCustomAttribute() != null) 117 | .ToList(); 118 | props = controllerType 119 | .GetProperties(bindingFlags) 120 | .Where(c => c.GetCustomAttribute() != null) 121 | .ToList(); 122 | } 123 | 124 | static string GetKey(MemberInfo member, Type controllerType) 125 | { 126 | return $"$ctrl-state_{controllerType.Name}.{member.Name}"; 127 | } 128 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/BotControllersInvoker.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class BotControllersInvoker 7 | { 8 | readonly ILogger _log; 9 | readonly IServiceProvider _services; 10 | readonly ArgumentBinder _binder; 11 | 12 | public BotControllersInvoker(ILogger log, IServiceProvider services, ArgumentBinder binder) 13 | { 14 | _log = log; 15 | _services = services; 16 | _binder = binder; 17 | } 18 | 19 | public async ValueTask Invoke(IUpdateContext ctx, CancellationToken token, MethodInfo method, params object[] args) 20 | { 21 | var controller = (BotController)_services.GetRequiredService(method.DeclaringType!); 22 | controller.Init(ctx, token); 23 | await InvokeInternal(controller, method, args, ctx, false); 24 | } 25 | 26 | public async ValueTask Invoke(IUpdateContext context) 27 | { 28 | var isPresented = context.Items.TryGetValue("controller", out var value) && value is BotController; 29 | if (!isPresented) 30 | { 31 | return false; 32 | } 33 | 34 | var controller = (BotController)value!; 35 | 36 | var method = (MethodInfo)context.Items["action"]; 37 | var args = (object[])context.Items["args"]; 38 | var skipBinding = context.Items.ContainsKey("skip_binding_marker"); 39 | await InvokeInternal(controller, method, args, context, !skipBinding); 40 | 41 | return true; 42 | } 43 | 44 | private async ValueTask InvokeInternal(BotController controller, MethodInfo method, object[] args, IUpdateContext ctx, bool bind = true) 45 | { 46 | var typedParams = bind ? await _binder.Bind(method, args, ctx) : args; 47 | 48 | _log.LogDebug("Begin execute action {Controller}.{Method}. Arguments: {@Args}", 49 | method.DeclaringType!.Name, 50 | method.Name, 51 | typedParams); 52 | 53 | await controller.OnBeforeCall(); 54 | 55 | var result = method.Invoke(controller, typedParams); 56 | if (result is Task task) 57 | { 58 | await task; 59 | } 60 | else if (result is ValueTask valueTask) 61 | { 62 | await valueTask; 63 | } 64 | 65 | await controller.OnAfterCall(); 66 | 67 | return result; 68 | } 69 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/BotRoutesExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public static class BotRoutesExtensions 6 | { 7 | public static string? GetAuthPolicy(this MethodInfo method) 8 | { 9 | if (method.GetCustomAttribute() != null) 10 | { 11 | return null; 12 | } 13 | 14 | var methodAuth = method.GetCustomAttribute(); 15 | if (methodAuth != null) 16 | { 17 | return methodAuth.Policy ?? string.Empty; 18 | } 19 | 20 | var classAuth = method.DeclaringType!.GetCustomAttribute(); 21 | if (classAuth != null) 22 | { 23 | return classAuth.Policy ?? string.Empty; 24 | } 25 | 26 | return null; 27 | } 28 | 29 | public static string? GetActionDescription(this MethodInfo method) 30 | { 31 | var action = method.GetCustomAttributes().FirstOrDefault(c => c.Desc != null); 32 | if (action == null) 33 | { 34 | return null; 35 | } 36 | 37 | return action.Desc; 38 | } 39 | 40 | public static string Truncate(this string str, int length) 41 | { 42 | if(str.Length <= length) 43 | { 44 | return str; 45 | } 46 | 47 | return str.Substring(0, length); 48 | } 49 | public static string TruncateEnd(this string str, int length) 50 | { 51 | if (str.Length <= length) 52 | { 53 | return str; 54 | } 55 | return str.Substring(str.Length - length - 1); 56 | } 57 | 58 | public static long GetDeterministicHashCode(this string str) 59 | { 60 | unchecked 61 | { 62 | long hash1 = (5381 << 16) + 5381; 63 | long hash2 = hash1; 64 | 65 | for (int i = 0; i < str.Length; i += 2) 66 | { 67 | hash1 = ((hash1 << 5) + hash1) ^ str[i]; 68 | if (i == str.Length - 1) 69 | break; 70 | hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; 71 | } 72 | 73 | return hash1 + (hash2 * 1566083941); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/BotServiceProvider.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Deployf.Botf; 4 | 5 | internal class BotServiceProvider : IBotServiceProvider 6 | { 7 | private readonly IServiceProvider? _container; 8 | private readonly IServiceScope? _scope; 9 | 10 | public BotServiceProvider(IApplicationBuilder app) 11 | { 12 | _container = app.ApplicationServices; 13 | } 14 | 15 | public BotServiceProvider(IServiceScope scope) 16 | { 17 | _scope = scope; 18 | } 19 | 20 | public object? GetService(Type serviceType) => 21 | _scope != null 22 | ? _scope.ServiceProvider.GetService(serviceType) 23 | : _container?.GetService(serviceType) 24 | ; 25 | 26 | public IBotServiceProvider CreateScope() => 27 | new BotServiceProvider(_container!.CreateScope()); 28 | 29 | public void Dispose() 30 | { 31 | _scope?.Dispose(); 32 | } 33 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/BotfBot.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot; 2 | using Telegram.Bot.Framework; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class BotfBot : BotBase 7 | { 8 | public readonly BotfOptions Options; 9 | 10 | public BotfBot(BotfOptions options) 11 | : base(options.Username!, new TelegramBotClient(new TelegramBotClientOptions(options.Token!, baseUrl: options.ApiBaseUrl))) 12 | { 13 | Options = options; 14 | } 15 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/BotfException.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Deployf.Botf; 4 | 5 | [Serializable] 6 | public class BotfException : Exception 7 | { 8 | public BotfException() { } 9 | public BotfException(string message) : base(message) { } 10 | public BotfException(string message, Exception inner) : base(message, inner) { } 11 | protected BotfException( 12 | SerializationInfo info, 13 | StreamingContext context) : base(info, context) { } 14 | } 15 | -------------------------------------------------------------------------------- /Deployf.Botf/System/BotfOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public class BotfOptions 4 | { 5 | private string? _username; 6 | 7 | public string? Token { get; set; } 8 | public string? Username 9 | { 10 | get => _username; 11 | set 12 | { 13 | _username = value; 14 | UsernameTag = "@" + value; 15 | } 16 | } 17 | 18 | public string? WebhookUrl { get; set; } 19 | public bool AutoSend { get; set; } = true; 20 | public bool HandleOnlyMentionedInGroups { get; set; } 21 | public string? ApiBaseUrl { get; set; } 22 | public bool AutoCleanReplyKeyboard { get; set; } 23 | public TimeSpan? ChainTimeout { get; set; } = TimeSpan.FromHours(1); 24 | public bool UseWebhooks => !string.IsNullOrEmpty(WebhookUrl); 25 | public string? WebAppUrl { get; set; } 26 | public string? WebhookPath 27 | { 28 | get 29 | { 30 | if (!string.IsNullOrEmpty(WebhookUrl)) 31 | { 32 | return new Uri(WebhookUrl).PathAndQuery; 33 | } 34 | 35 | return null; 36 | } 37 | } 38 | 39 | public string? UsernameTag { get; private set; } 40 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/ChainStorage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | public class ChainStorage 7 | { 8 | readonly IDictionary _chains; 9 | 10 | public ChainStorage() 11 | { 12 | _chains = new ConcurrentDictionary(); 13 | } 14 | 15 | public ChainItem? Get(long id) 16 | { 17 | _chains.TryGetValue(id, out var result); 18 | return result; 19 | } 20 | 21 | public void Clear(long id) 22 | { 23 | if(_chains.ContainsKey(id)) 24 | { 25 | _chains[id] = null; 26 | _chains.Remove(id); 27 | } 28 | } 29 | 30 | public void Set(long id, ChainItem item) 31 | { 32 | _chains[id] = item; 33 | } 34 | 35 | public record ChainItem(TaskCompletionSource Synchronizator); 36 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/ChainTimeoutException.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public class ChainTimeoutException : Exception 4 | { 5 | public readonly bool Handled; 6 | 7 | public ChainTimeoutException(bool handled) 8 | { 9 | Handled = handled; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Deployf.Botf/System/ConnectionString.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public class ConnectionString 4 | { 5 | /// 6 | /// token?key=value&key=value... 7 | /// 8 | /// 9 | /// 10 | /// 11 | public static BotfOptions Parse(string value) 12 | { 13 | if(string.IsNullOrEmpty(value)) 14 | { 15 | throw new ArgumentNullException("value"); 16 | } 17 | 18 | var options = new BotfOptions(); 19 | 20 | var main = value.Split('?', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); 21 | 22 | if(main.Length > 0) 23 | { 24 | options.Token = main[0]; 25 | } 26 | else 27 | { 28 | throw new BotfException("Connection string for BotF (configuration string) is empty or has only whitespaces."); 29 | } 30 | 31 | if(main.Length == 1) 32 | { 33 | return options; 34 | } 35 | 36 | var values = main[1].Split('&', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); 37 | foreach (var kv in values) 38 | { 39 | var cortage = kv.Split('=', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); 40 | if(cortage == null || cortage.Length != 2) 41 | { 42 | throw new BotfException("Botf connection string is wrong. It must have format like `bot_token?key1=value1&key2=value2..`"); 43 | } 44 | 45 | switch (cortage[0]) 46 | { 47 | case "botname": 48 | options.Username = cortage[1]; 49 | break; 50 | case "autosend": 51 | if(bool.TryParse(cortage[1], out var autosend)) 52 | { 53 | options.AutoSend = autosend; 54 | } 55 | else 56 | { 57 | throw new BotfException("`autosend` configuration option has wrong format, it should be a bool convertable value like true|false|1|0|True|False"); 58 | } 59 | break; 60 | case "group_mode": 61 | if (bool.TryParse(cortage[1], out var groupMode)) 62 | { 63 | options.HandleOnlyMentionedInGroups = groupMode; 64 | } 65 | else 66 | { 67 | throw new BotfException("`group_mode` configuration option has wrong format, it should be a bool convertable value like true|false|1|0|True|False"); 68 | } 69 | break; 70 | case "webhook": 71 | options.WebhookUrl = cortage[1]; 72 | break; 73 | case "api": 74 | options.ApiBaseUrl = cortage[1]; 75 | break; 76 | case "autoclean": 77 | if (bool.TryParse(cortage[1], out var autoclean)) 78 | { 79 | options.AutoCleanReplyKeyboard = autoclean; 80 | } 81 | else 82 | { 83 | throw new BotfException("`autoclean` configuration option has wrong format, it should be a bool convertable value like true|false|1|0|True|False"); 84 | } 85 | break; 86 | case "chain_timeout": 87 | options.ChainTimeout = cortage[1].TryParseTimeSpan(); 88 | break; 89 | case "webapp_url": 90 | options.WebAppUrl = cortage[1]; 91 | break; 92 | } 93 | } 94 | 95 | return options; 96 | } 97 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/CustomUpdatePollingManager.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot; 2 | using Telegram.Bot.Framework; 3 | using Telegram.Bot.Framework.Abstractions; 4 | using Telegram.Bot.Requests; 5 | using Telegram.Bot.Types; 6 | using Telegram.Bot.Types.Enums; 7 | 8 | namespace Deployf.Botf; 9 | 10 | public class CustomUpdatePollingManager : IUpdatePollingManager where TBot : IBot 11 | { 12 | private readonly UpdateDelegate _updateDelegate; 13 | private readonly IBotServiceProvider _rootProvider; 14 | 15 | public CustomUpdatePollingManager(IBotBuilder botBuilder, IBotServiceProvider rootProvider) 16 | { 17 | _updateDelegate = botBuilder.Build(); 18 | _rootProvider = rootProvider; 19 | } 20 | 21 | public async Task RunAsync( 22 | GetUpdatesRequest? requestParams = null, 23 | CancellationToken cancellationToken = default) 24 | { 25 | var bot = (TBot)_rootProvider.GetRequiredService(typeof(TBot)); 26 | await bot.Client.DeleteWebhookAsync(false, cancellationToken); 27 | 28 | var getUpdatesRequest = requestParams ?? new GetUpdatesRequest 29 | { 30 | Offset = 0, 31 | Timeout = 500, 32 | AllowedUpdates = Array.Empty() 33 | }; 34 | requestParams = getUpdatesRequest; 35 | 36 | while (!cancellationToken.IsCancellationRequested) 37 | { 38 | var updates = await bot.Client.MakeRequestAsync(requestParams, cancellationToken); 39 | 40 | foreach (var item in updates) 41 | { 42 | ProcessUpdate(bot, item, cancellationToken); 43 | } 44 | 45 | if (updates.Length != 0) 46 | requestParams.Offset = updates[updates.Length - 1].Id + 1; 47 | } 48 | 49 | cancellationToken.ThrowIfCancellationRequested(); 50 | } 51 | 52 | async void ProcessUpdate(TBot bot, Update item, CancellationToken cancellationToken) 53 | { 54 | using IBotServiceProvider scopeProvider = _rootProvider.CreateScope(); 55 | await _updateDelegate(new UpdateContext(bot, item, scopeProvider), cancellationToken); 56 | } 57 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/GlobalStateService.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework; 2 | using Telegram.Bot.Framework.Abstractions; 3 | using Telegram.Bot.Types; 4 | 5 | namespace Deployf.Botf; 6 | 7 | public class GlobalStateService : IGlobalStateService 8 | { 9 | public readonly IKeyValueStorage Store; 10 | public readonly IBot Bot; 11 | public readonly IServiceProvider Provider; 12 | public readonly BotUserService TokenService; 13 | public readonly BotControllerHandlers Handlers; 14 | public readonly BotControllerRoutes Routes; 15 | public readonly BotControllersInvoker Invoker; 16 | 17 | public GlobalStateService(IKeyValueStorage store, BotfBot bot, IServiceProvider provider, BotUserService tokenService, BotControllerHandlers handlers, BotControllerRoutes routes, BotControllersInvoker invoker) 18 | { 19 | Store = store; 20 | Bot = bot; 21 | Provider = provider; 22 | TokenService = tokenService; 23 | Handlers = handlers; 24 | Routes = routes; 25 | Invoker = invoker; 26 | } 27 | 28 | public async Task SetState(long userId, T newState, bool callEnter = true, bool callLeave = true, CancellationToken cancelToken = default) 29 | { 30 | var context = new UpdateContext(Bot, new Update(), Provider) 31 | { 32 | UserId = userId, 33 | ChatId = userId 34 | }; 35 | var user = await TokenService.GetUser(userId); 36 | if(newState == null) 37 | { 38 | if(await Store!.Contain(userId, Consts.GLOBAL_STATE)) 39 | { 40 | var oldState = await Store!.Get(userId, Consts.GLOBAL_STATE, null); 41 | if (callLeave && oldState != null) 42 | { 43 | await Call(true, oldState); 44 | } 45 | } 46 | await Store!.Remove(userId, Consts.GLOBAL_STATE); 47 | await CallClear(); 48 | } 49 | else 50 | { 51 | if (await Store!.Contain(userId, Consts.GLOBAL_STATE)) 52 | { 53 | var oldState = await Store!.Get(userId, Consts.GLOBAL_STATE, null); 54 | if(callEnter && oldState != null) 55 | { 56 | await Call(true, oldState); 57 | } 58 | } 59 | await Store!.Set(userId, Consts.GLOBAL_STATE, newState); 60 | await Call(false, newState); 61 | } 62 | 63 | async ValueTask Call(bool leave, object oldState) 64 | { 65 | var controllerType = Routes.GetStateType(oldState.GetType()); 66 | if (controllerType != null) 67 | { 68 | var controller = (BotControllerState)context!.Services.GetRequiredService(controllerType); 69 | controller.Init(context, cancelToken); 70 | controller.User = user; 71 | await controller.OnBeforeCall(); 72 | if (leave) 73 | { 74 | await controller.OnLeave(); 75 | } 76 | else 77 | { 78 | await controller.OnEnter(); 79 | } 80 | await controller.OnAfterCall(); 81 | } 82 | } 83 | 84 | async ValueTask CallClear() 85 | { 86 | var handlersContainer = Handlers; 87 | var lookup = handlersContainer.GetHandlers(Handle.ClearState); 88 | if(lookup == null) 89 | { 90 | return; 91 | } 92 | 93 | foreach (var handler in lookup) 94 | { 95 | if(context.IsHandlingStopRequested()) 96 | { 97 | break; 98 | } 99 | if(handler.TryFilter(context)) 100 | { 101 | await Invoker.Invoke(context, cancelToken, handler.TargetMethod); 102 | } 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/IBotContextAccessor.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public interface IBotContextAccessor 6 | { 7 | IUpdateContext? Context { get; set; } 8 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/IGlobalStateService.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public interface IGlobalStateService 4 | { 5 | Task SetState(long userId, T newState, bool callEnter = true, bool callLeave = true, CancellationToken cancelToken = default); 6 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/IKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public interface IKeyGenerator 4 | { 5 | long GetLongId(); 6 | Guid GetGuidId(); 7 | } 8 | 9 | public class RandomKeyGenerator : IKeyGenerator 10 | { 11 | public Guid GetGuidId() 12 | { 13 | return Guid.NewGuid(); 14 | } 15 | 16 | public long GetLongId() 17 | { 18 | return Random.Shared.NextInt64(); 19 | } 20 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/IKeyValueStorage.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public interface IKeyValueStorage 4 | { 5 | ValueTask Get(long userId, string key, T? defaultValue); 6 | ValueTask Get(long userId, string key, object? defaultValue); 7 | ValueTask Set(long userId, string key, object value); 8 | ValueTask Remove(long userId, string key); 9 | ValueTask Contain(long userId, string key); 10 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/InMemoryKeyValueStorage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public class InMemoryKeyValueStorage : IKeyValueStorage 6 | { 7 | readonly IDictionary _store; 8 | 9 | public InMemoryKeyValueStorage() 10 | { 11 | _store = new ConcurrentDictionary(); 12 | } 13 | 14 | public ValueTask Contain(long userId, string key) 15 | { 16 | var realKey = GetRealKey(userId, key); 17 | return new(_store.ContainsKey(realKey)); 18 | } 19 | 20 | public ValueTask Get(long userId, string key, T? defaultValue) 21 | { 22 | var realKey = GetRealKey(userId, key); 23 | if(_store.TryGetValue(realKey, out var value)) 24 | { 25 | return new ((T)value); 26 | } 27 | 28 | return new (defaultValue); 29 | } 30 | 31 | public ValueTask Get(long userId, string key, object? defaultValue) 32 | { 33 | var realKey = GetRealKey(userId, key); 34 | if (_store.TryGetValue(realKey, out var value)) 35 | { 36 | return new(value); 37 | } 38 | 39 | return new(defaultValue); 40 | } 41 | 42 | public async ValueTask Remove(long userId, string key) 43 | { 44 | if(await Contain(userId, key)) 45 | { 46 | var realKey = GetRealKey(userId, key); 47 | _store.Remove(realKey); 48 | } 49 | } 50 | 51 | public ValueTask Set(long userId, string key, object value) 52 | { 53 | var realKey = GetRealKey(userId, key); 54 | _store[realKey] = value!; 55 | return ValueTask.CompletedTask; 56 | } 57 | 58 | string GetRealKey(long userId, string key) 59 | { 60 | return userId.ToString() + "/" + key; 61 | } 62 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/NumberExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public static class NumberExtensions 4 | { 5 | public static long Base64(this string base64) 6 | { 7 | if (base64.Length % 4 != 0) 8 | { 9 | base64 += "===".Substring(0, 4 - (base64.Length % 4)); 10 | } 11 | var bytes = Convert.FromBase64String(base64.Replace("-", "/")); 12 | return BitConverter.ToInt64(bytes); 13 | } 14 | 15 | public static string Base64(this long value) 16 | { 17 | var bytes = BitConverter.GetBytes(value); 18 | return Convert.ToBase64String(bytes).Replace("/", "-").Replace("=", ""); 19 | } 20 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/Paging/PageFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public class PageFilter 4 | { 5 | public int? Page { get; set; } 6 | public int? Count { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /Deployf.Botf/System/Paging/Paging.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public class Paging 4 | { 5 | public int Count { get; set; } 6 | public int ItemsPerPage { get; set; } 7 | public int PageNumber { get; set; } 8 | public IEnumerable Items { get; set; } 9 | 10 | public Paging(int count, int itemsPerPage, int page, IEnumerable items) 11 | { 12 | Count = count; 13 | ItemsPerPage = itemsPerPage; 14 | PageNumber = page; 15 | Items = items; 16 | } 17 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/Paging/PagingService.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public class PagingService 4 | { 5 | public int PageDefaultCount = 15; 6 | 7 | public PagingService() 8 | { 9 | } 10 | 11 | public PagingService(int pageDefaultCount) 12 | { 13 | PageDefaultCount = pageDefaultCount; 14 | } 15 | 16 | public Paging Paging(IQueryable collection, PageFilter pageParams) 17 | { 18 | var count = collection.Count(); 19 | var skiping = (pageParams.Count ?? PageDefaultCount) * (pageParams.Page ?? 0); 20 | var taking = pageParams.Count ?? PageDefaultCount; 21 | var resultCollection = collection.Skip(skiping).Take(taking); 22 | return new Paging(count, pageParams.Count ?? PageDefaultCount, pageParams.Page ?? 0, resultCollection); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Deployf.Botf/System/RouteStateSkipFunction.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace Deployf.Botf; 5 | 6 | //TODO: Refactor to instance class 7 | internal static class RouteStateSkipFunction 8 | { 9 | static readonly Type STATE_TYPE = typeof(BotControllerState); 10 | 11 | internal static RouteSkipDelegate? SkipFunctionFactory(bool hasStates, MethodInfo action) 12 | { 13 | if (!hasStates) 14 | { 15 | return null; 16 | } 17 | 18 | return SkipFunction; 19 | } 20 | 21 | private static async ValueTask SkipFunction(string key, RouteInfo info, IUpdateContext ctx) 22 | { 23 | var fromId = ctx.GetSafeUserId(); 24 | if (!fromId.HasValue) 25 | { 26 | return true; 27 | } 28 | 29 | var store = ctx.Services.GetRequiredService(); 30 | var isStateController = STATE_TYPE.IsAssignableFrom(info.Method.DeclaringType); 31 | var hasState = await store.Contain(fromId!.Value, Consts.GLOBAL_STATE); 32 | if (!hasState) 33 | { 34 | return isStateController; 35 | } 36 | else if(!isStateController) 37 | { 38 | return true; 39 | } 40 | 41 | var state = await store.Get(fromId!.Value, Consts.GLOBAL_STATE, null); 42 | if (state == null) 43 | { 44 | return true; 45 | } 46 | 47 | var stateType = info.Method.DeclaringType!.BaseType!.GenericTypeArguments[0]; // TODO: implement getting base type 48 | return !state.GetType().IsAssignableFrom(stateType); 49 | } 50 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public static class StringExtensions 4 | { 5 | public static bool IsUrl(this string data) 6 | { 7 | return data.StartsWith("https://") || data.StartsWith("http://"); 8 | } 9 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/TimeSpanFormatExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public static class TimeSpanFormatExtensions 4 | { 5 | public static TimeSpan? TryParseTimeSpan(this string format) 6 | { 7 | if(format == "-1") 8 | { 9 | return null; 10 | } 11 | 12 | if (TimeSpan.TryParse(format, out var result)) 13 | { 14 | return result; 15 | } 16 | else 17 | { 18 | throw new FormatException("TimeSpan wrong format"); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Deployf.Botf/System/UpdateMessageStrategies/EditTextMessageStrategy.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot; 2 | using Telegram.Bot.Types; 3 | 4 | namespace Deployf.Botf.System.UpdateMessageStrategies; 5 | 6 | /// 7 | /// Situation: previous message has no media file and a new message does not have. 8 | /// 9 | public class EditTextMessageStrategy : IUpdateMessageStrategy 10 | { 11 | private readonly BotfBot _bot; 12 | 13 | public EditTextMessageStrategy(BotfBot bot) 14 | { 15 | _bot = bot; 16 | } 17 | 18 | public bool CanHandle(IUpdateMessageContext context) 19 | { 20 | var newMessageFileIsEmpty = context.MediaFile is InputMediaDocument; 21 | 22 | return context.PreviousMessage.Photo == null && newMessageFileIsEmpty; 23 | } 24 | 25 | public async Task UpdateMessage(IUpdateMessageContext context) 26 | { 27 | return await _bot.Client.EditMessageTextAsync( 28 | context.ChatId, 29 | context.MessageId, 30 | context.MessageText, 31 | parseMode: context.ParseMode, 32 | replyMarkup: context.KeyboardMarkup, 33 | cancellationToken: context.CancelToken 34 | ); 35 | } 36 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/UpdateMessageStrategies/IUpdateMessageStrategy.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Types; 2 | 3 | namespace Deployf.Botf.System.UpdateMessageStrategies; 4 | 5 | public interface IUpdateMessageStrategy 6 | { 7 | public bool CanHandle(IUpdateMessageContext context); 8 | public Task UpdateMessage(IUpdateMessageContext context); 9 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/UpdateMessageStrategies/IUpdateMessageStrategyFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf.System.UpdateMessageStrategies; 2 | 3 | public interface IUpdateMessageStrategyFactory 4 | { 5 | IUpdateMessageStrategy? GetStrategy(IUpdateMessageContext context); 6 | } 7 | 8 | public class UpdateMessageStrategyFactory : IUpdateMessageStrategyFactory 9 | { 10 | private readonly IEnumerable _strategies; 11 | 12 | public UpdateMessageStrategyFactory(IEnumerable strategies) 13 | { 14 | _strategies = strategies; 15 | } 16 | 17 | public IUpdateMessageStrategy? GetStrategy(IUpdateMessageContext context) 18 | { 19 | return _strategies.FirstOrDefault(s => s.CanHandle(context)); 20 | } 21 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/UpdateMessageStrategies/MediaToMediaFileStrategy.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot; 2 | using Telegram.Bot.Types; 3 | 4 | namespace Deployf.Botf.System.UpdateMessageStrategies; 5 | 6 | /// 7 | /// Situation: previous message has a media file and a new message has one. 8 | /// 9 | public class MediaToMediaFileStrategy : IUpdateMessageStrategy 10 | { 11 | private readonly BotfBot _bot; 12 | 13 | public MediaToMediaFileStrategy(BotfBot bot) 14 | { 15 | _bot = bot; 16 | } 17 | 18 | public bool CanHandle(IUpdateMessageContext context) 19 | { 20 | var newMessageHasFile = context.MediaFile is InputMediaDocument; 21 | 22 | return context.PreviousMessage.Photo != null && newMessageHasFile; 23 | } 24 | 25 | public async Task UpdateMessage(IUpdateMessageContext context) 26 | { 27 | var updateMessagePolicy = context.UpdateContext.GetCurrentUpdateMsgPolicy(); 28 | if (updateMessagePolicy is UpdateMessagePolicy.DeleteAndSend) 29 | { 30 | await _bot.Client.DeleteMessageAsync(context.ChatId, context.PreviousMessage.MessageId, context.CancelToken); 31 | return await _bot.Client.SendPhotoAsync( 32 | context.ChatId, 33 | context.MediaFile!.Media, 34 | null, 35 | context.MessageText, 36 | context.ParseMode, 37 | replyMarkup: context.KeyboardMarkup, 38 | cancellationToken: context.CancelToken, 39 | replyToMessageId: context.ReplyToMessageId); 40 | } 41 | else 42 | { 43 | return await _bot.Client.EditMessageMediaAsync( 44 | context.ChatId, 45 | context.MessageId, 46 | new InputMediaPhoto(context.MediaFile!.Media) 47 | { 48 | Caption = context.MessageText, 49 | ParseMode = context.ParseMode 50 | }, 51 | replyMarkup: context.KeyboardMarkup, 52 | cancellationToken: context.CancelToken 53 | ); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/UpdateMessageStrategies/MediaToPlainTextStrategy.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot; 2 | using Telegram.Bot.Types; 3 | 4 | namespace Deployf.Botf.System.UpdateMessageStrategies; 5 | 6 | /// 7 | /// Situation: previous message has media file, but a new message does not have. 8 | /// 9 | public class MediaToPlainTextStrategy : IUpdateMessageStrategy 10 | { 11 | private readonly BotfBot _bot; 12 | 13 | public MediaToPlainTextStrategy(BotfBot bot) 14 | { 15 | _bot = bot; 16 | } 17 | 18 | public bool CanHandle(IUpdateMessageContext context) 19 | { 20 | var newMessageFileIsEmpty = context.MediaFile is InputMediaDocument; 21 | 22 | return context.PreviousMessage.Photo != null && newMessageFileIsEmpty; 23 | } 24 | 25 | public async Task UpdateMessage(IUpdateMessageContext context) 26 | { 27 | await _bot.Client.DeleteMessageAsync(context.ChatId, context.PreviousMessage.MessageId, context.CancelToken); 28 | return await _bot.Client.SendTextMessageAsync( 29 | context.ChatId, 30 | context.MessageText, 31 | null, 32 | context.ParseMode, 33 | replyMarkup: context.KeyboardMarkup, 34 | cancellationToken: context.CancelToken, 35 | replyToMessageId: context.ReplyToMessageId); 36 | } 37 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/UpdateMessageStrategies/PlainTextToMediaStrategy.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot; 2 | using Telegram.Bot.Types; 3 | 4 | namespace Deployf.Botf.System.UpdateMessageStrategies; 5 | 6 | /// 7 | /// Situation: previous message has no media file, but a new message has one. 8 | /// 9 | public class PlainTextToMediaStrategy : IUpdateMessageStrategy 10 | { 11 | private readonly BotfBot _bot; 12 | 13 | public PlainTextToMediaStrategy(BotfBot bot) 14 | { 15 | _bot = bot; 16 | } 17 | 18 | public bool CanHandle(IUpdateMessageContext context) 19 | { 20 | var newMessageHasFile = context.MediaFile is InputMediaDocument; 21 | 22 | return context.PreviousMessage.Photo == null && newMessageHasFile; 23 | } 24 | 25 | public async Task UpdateMessage(IUpdateMessageContext context) 26 | { 27 | await _bot.Client.DeleteMessageAsync(context.ChatId, context.PreviousMessage.MessageId, context.CancelToken); 28 | return await _bot.Client.SendPhotoAsync( 29 | context.ChatId, 30 | context.MediaFile!.Media, 31 | null, 32 | context.MessageText, 33 | context.ParseMode, 34 | replyMarkup: context.KeyboardMarkup, 35 | cancellationToken: context.CancelToken, 36 | replyToMessageId: context.ReplyToMessageId); 37 | } 38 | } -------------------------------------------------------------------------------- /Deployf.Botf/System/UpdateMessageStrategies/UpdateMessageContext.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | using Telegram.Bot.Types; 3 | using Telegram.Bot.Types.Enums; 4 | using Telegram.Bot.Types.ReplyMarkups; 5 | 6 | namespace Deployf.Botf.System.UpdateMessageStrategies; 7 | 8 | public interface IUpdateMessageContext 9 | { 10 | public IUpdateContext UpdateContext { get; init; } 11 | public long ChatId { get; init; } 12 | public int MessageId { get; init; } 13 | public string MessageText { get; init; } 14 | public Message PreviousMessage { get; init; } 15 | public InputMedia? MediaFile { get; init; } 16 | public InlineKeyboardMarkup? KeyboardMarkup { get; init; } 17 | public ParseMode ParseMode { get; init; } 18 | public int ReplyToMessageId { get; init; } 19 | public CancellationToken CancelToken { get; init; } 20 | } 21 | 22 | public record UpdateMessageContext(IUpdateContext UpdateContext, long ChatId, int MessageId, string MessageText, Message PreviousMessage, 23 | InputMedia? MediaFile, InlineKeyboardMarkup? KeyboardMarkup, ParseMode ParseMode, int ReplyToMessageId, 24 | CancellationToken CancelToken) : IUpdateMessageContext; -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/ASP.NET Core/TelegramBotMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Telegram.Bot.Framework.Abstractions; 3 | using Telegram.Bot.Types; 4 | 5 | namespace Telegram.Bot.Framework 6 | { 7 | internal class TelegramBotMiddleware 8 | where TBot : BotBase 9 | { 10 | private readonly RequestDelegate _next; 11 | 12 | private readonly UpdateDelegate _updateDelegate; 13 | 14 | private readonly ILogger> _logger; 15 | 16 | /// 17 | /// Initializes an instance of middleware 18 | /// 19 | /// Instance of request delegate 20 | /// Logger for this middleware 21 | public TelegramBotMiddleware( 22 | RequestDelegate next, 23 | UpdateDelegate updateDelegate, 24 | ILogger> logger 25 | ) 26 | { 27 | _next = next; 28 | _updateDelegate = updateDelegate; 29 | _logger = logger; 30 | } 31 | 32 | /// 33 | /// Gets invoked to handle the incoming request 34 | /// 35 | /// 36 | public async Task Invoke(HttpContext context) 37 | { 38 | if (context.Request.Method != HttpMethods.Post) 39 | { 40 | await _next.Invoke(context) 41 | .ConfigureAwait(false); 42 | return; 43 | } 44 | 45 | string payload; 46 | using (var reader = new StreamReader(context.Request.Body)) 47 | { 48 | payload = await reader.ReadToEndAsync() 49 | .ConfigureAwait(false); 50 | } 51 | 52 | _logger.LogDebug("Update payload:\n{0}", payload); 53 | 54 | Update? update = null; 55 | try 56 | { 57 | update = JsonConvert.DeserializeObject(payload); 58 | } 59 | catch (JsonException e) 60 | { 61 | _logger.LogError($"Unable to deserialize update payload. {e.Message}"); 62 | } 63 | 64 | if (update == null) 65 | { 66 | // it is unlikely of Telegram to send an invalid update object. 67 | // respond with "404 Not Found" in case an attacker is trying to find the webhook URL 68 | context.Response.StatusCode = 404; 69 | return; 70 | } 71 | 72 | using (var scope = context.RequestServices.CreateScope()) 73 | { 74 | var bot = scope.ServiceProvider.GetRequiredService(); 75 | var updateContext = new UpdateContext(bot, update, scope.ServiceProvider); 76 | updateContext.Items.Add(nameof(HttpContext), context); 77 | 78 | try 79 | { 80 | await _updateDelegate(updateContext) 81 | .ConfigureAwait(false); 82 | } 83 | catch (Exception e) 84 | { 85 | _logger.LogError($"Error occured while handling update `{update.Id}`. {e.Message}"); 86 | context.Response.StatusCode = StatusCodes.Status500InternalServerError; 87 | } 88 | } 89 | 90 | if (!context.Response.HasStarted) 91 | { 92 | context.Response.StatusCode = 201; 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/Abstractions/IBot.cs: -------------------------------------------------------------------------------- 1 | namespace Telegram.Bot.Framework.Abstractions 2 | { 3 | /// 4 | /// A wrapper around TelegramBot class. Used to make calls to the Bot API 5 | /// 6 | public interface IBot 7 | { 8 | string Username { get; } 9 | 10 | /// 11 | /// Instance of Telegram bot client 12 | /// 13 | ITelegramBotClient Client { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/Abstractions/IBotBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Telegram.Bot.Framework.Abstractions 2 | { 3 | public interface IBotBuilder 4 | { 5 | IBotBuilder Use(Func middleware); 6 | 7 | IBotBuilder Use() 8 | where THandler : IUpdateHandler; 9 | 10 | IBotBuilder Use(THandler handler) 11 | where THandler : IUpdateHandler; 12 | 13 | UpdateDelegate Build(); 14 | } 15 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/Abstractions/IBotOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Telegram.Bot.Framework.Abstractions 2 | { 3 | /// 4 | /// Configurations for the bot 5 | /// 6 | public interface IBotOptions 7 | { 8 | string Username { get; } 9 | 10 | /// 11 | /// Optional if client not needed. Telegram API token 12 | /// 13 | string ApiToken { get; } 14 | 15 | string WebhookPath { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/Abstractions/IBotServiceProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Telegram.Bot.Framework.Abstractions 2 | { 3 | public interface IBotServiceProvider : IServiceProvider, IDisposable 4 | { 5 | IBotServiceProvider CreateScope(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/Abstractions/IUpdateContext.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Types; 2 | 3 | namespace Telegram.Bot.Framework.Abstractions 4 | { 5 | public interface IUpdateContext 6 | { 7 | IBot Bot { get; } 8 | 9 | Update Update { get; } 10 | 11 | IServiceProvider Services { get; } 12 | 13 | IDictionary Items { get; } 14 | 15 | long? UserId { get; set; } 16 | long? ChatId { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/Abstractions/IUpdateHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Telegram.Bot.Framework.Abstractions 2 | { 3 | /// 4 | /// Processes an update 5 | /// 6 | public interface IUpdateHandler 7 | { 8 | /// 9 | /// Handles the update for bot. This method will be called only if CanHandleUpdate returns true 10 | /// 11 | /// Instance of the bot this command is operating for 12 | /// The update to be handled 13 | /// Result of handling this update 14 | Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken); 15 | } 16 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/Abstractions/IUpdatePollingManager.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Requests; 2 | 3 | namespace Telegram.Bot.Framework.Abstractions 4 | { 5 | public interface IUpdatePollingManager 6 | where TBot : IBot 7 | { 8 | Task RunAsync( 9 | GetUpdatesRequest? requestParams = default, 10 | CancellationToken cancellationToken = default 11 | ); 12 | } 13 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/Abstractions/UpdateDelegate.cs: -------------------------------------------------------------------------------- 1 | namespace Telegram.Bot.Framework.Abstractions 2 | { 3 | public delegate Task UpdateDelegate(IUpdateContext context, CancellationToken cancellationToken = default); 4 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/BotBase.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Telegram.Bot.Framework 4 | { 5 | public abstract class BotBase : IBot 6 | { 7 | public ITelegramBotClient Client { get; } 8 | 9 | public string Username { get; } 10 | 11 | protected BotBase(string username, ITelegramBotClient client) 12 | { 13 | Username = username; 14 | Client = client; 15 | } 16 | 17 | protected BotBase(string username, string token) 18 | : this(username, new TelegramBotClient(token)) 19 | { 20 | } 21 | 22 | protected BotBase(IBotOptions options) 23 | : this(options.Username, new TelegramBotClient(options.ApiToken)) 24 | { 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/BotOptions.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Telegram.Bot.Framework 4 | { 5 | /// 6 | /// Configurations for the bot 7 | /// 8 | /// Type of Bot 9 | public class BotOptions : IBotOptions 10 | where TBot : IBot 11 | { 12 | public string Username { get; set; } 13 | 14 | /// 15 | /// Optional if client not needed. Telegram API token 16 | /// 17 | public string ApiToken { get; set; } 18 | 19 | public string WebhookPath { get; set; } 20 | 21 | public BotOptions(string username, string apiToken, string webhookPath) 22 | { 23 | Username = username; 24 | ApiToken = apiToken; 25 | WebhookPath = webhookPath; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/Update Pipeline/BotBuilder.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Telegram.Bot.Framework 4 | { 5 | public class BotBuilder : IBotBuilder 6 | { 7 | internal UpdateDelegate? UpdateDelegate { get; private set; } 8 | 9 | private readonly ICollection> _components; 10 | 11 | public BotBuilder() 12 | { 13 | _components = new List>(); 14 | } 15 | 16 | public IBotBuilder Use(Func middleware) 17 | { 18 | _components.Add(middleware); 19 | return this; 20 | } 21 | 22 | public IBotBuilder Use() 23 | where THandler : IUpdateHandler 24 | { 25 | _components.Add( 26 | next => 27 | (context, cancellationToken) => 28 | { 29 | if (context.Services.GetService(typeof(THandler)) is IUpdateHandler handler) 30 | return handler.HandleAsync(context, next, cancellationToken); 31 | else 32 | throw new NullReferenceException( 33 | $"Unable to resolve handler of type {typeof(THandler).FullName}" 34 | ); 35 | } 36 | ); 37 | 38 | return this; 39 | } 40 | 41 | public IBotBuilder Use(UpdateDelegate component) 42 | { 43 | _components.Add(next => component); 44 | 45 | return this; 46 | } 47 | 48 | public IBotBuilder Use(THandler handler) 49 | where THandler : IUpdateHandler 50 | { 51 | _components.Add(next => 52 | (context, cancellationToken) => handler.HandleAsync(context, next, cancellationToken) 53 | ); 54 | 55 | return this; 56 | } 57 | 58 | public UpdateDelegate Build() 59 | { 60 | UpdateDelegate handle = (context, cancellationToken) => 61 | { 62 | // use Logger 63 | Console.WriteLine("No handler for update {0} of type {1}.", context.Update.Id, context.Update.Type); 64 | return Task.FromResult(1); 65 | }; 66 | 67 | foreach (var component in _components.Reverse()) 68 | { 69 | handle = component(handle); 70 | } 71 | 72 | return UpdateDelegate = handle; 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /Deployf.Botf/Telegram.Bot.Framework/UpdateContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Telegram.Bot.Framework.Abstractions; 3 | using Telegram.Bot.Types; 4 | 5 | namespace Telegram.Bot.Framework 6 | { 7 | public class UpdateContext : IUpdateContext 8 | { 9 | public IBot Bot { get; } 10 | 11 | public Update Update { get; } 12 | 13 | public IServiceProvider Services { get; } 14 | 15 | public IDictionary Items { get; } 16 | 17 | public long? UserId { get; set; } 18 | public long? ChatId { get; set; } 19 | 20 | public UpdateContext(IBot bot, Update u, IServiceProvider services) 21 | { 22 | Bot = bot; 23 | Update = u; 24 | Services = services; 25 | Items = new ConcurrentDictionary(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Deployf.Botf/Users/BotUserService.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework.Abstractions; 2 | 3 | namespace Deployf.Botf; 4 | 5 | public class BotUserService 6 | { 7 | readonly IBotUserService? _userService; 8 | private readonly ILogger _log; 9 | 10 | public BotUserService(IBotUserService? userService, ILogger log) 11 | { 12 | _userService = userService; 13 | _log = log; 14 | } 15 | 16 | public BotUserService(ILogger log) 17 | { 18 | _log = log; 19 | } 20 | 21 | public BotUserService() 22 | { 23 | } 24 | 25 | public async ValueTask<(string? id, string[]? roles)> GetUserIdWithRoles(long tgUserId) 26 | { 27 | if(_userService != null) 28 | { 29 | return await _userService.GetUserIdWithRoles(tgUserId); 30 | } 31 | return (null, null); 32 | } 33 | 34 | public async Task GetUser(long? tgUserId) 35 | { 36 | if (!tgUserId.HasValue) 37 | { 38 | _log.LogWarning("Telegram userId not found!"); 39 | return new UserClaims(); 40 | } 41 | 42 | var (userId, roles) = await GetUserIdWithRoles(tgUserId.Value); 43 | if (userId == null) 44 | { 45 | _log.LogDebug("User with {tgUserId} not found in database", tgUserId.Value); 46 | return new UserClaims(); 47 | } 48 | 49 | var claim = new UserClaims() 50 | { 51 | Roles = roles ?? new string[0], 52 | IsAuthorized = true, 53 | Id = userId 54 | }; 55 | 56 | _log.LogDebug("User {@User} with {tgUserId} found", claim, tgUserId.Value); 57 | return claim; 58 | } 59 | } -------------------------------------------------------------------------------- /Deployf.Botf/Users/IBotUserService.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public interface IBotUserService 4 | { 5 | ValueTask<(string? id, string[]? roles)> GetUserIdWithRoles(long tgUserId); 6 | } -------------------------------------------------------------------------------- /Deployf.Botf/Users/UserClaims.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf; 2 | 3 | public class UserClaims 4 | { 5 | static readonly string[] _emptyRoles = new string[0]; 6 | 7 | public bool IsAuthorized { get; set; } 8 | public string Id { get; set; } = string.Empty; 9 | public string[] Roles { get; set; } = _emptyRoles; 10 | 11 | public bool IsInRole(string role) => Roles.Contains(role); 12 | 13 | public static implicit operator string(UserClaims user) 14 | { 15 | return user.Id; 16 | } 17 | } -------------------------------------------------------------------------------- /Deployf.Botf/deployf_logo_sq_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deploy-f/botf/aa8144dd316ff5acd95f5a6edf2419d6af423e65/Deployf.Botf/deployf_logo_sq_128.png -------------------------------------------------------------------------------- /Examples/.gitignore: -------------------------------------------------------------------------------- 1 | **/global.json -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ActionButtonsExample/Deployf.Botf.ActionButtonsExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ActionButtonsExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | 3 | BotfProgram.StartBot(args); 4 | 5 | class ActionsController : BotController 6 | { 7 | [Action("/start", "start")] 8 | void Start() 9 | { 10 | var guid = Guid.NewGuid(); 11 | var q = Q(GuidArgumentButtonHandler, guid); 12 | Push($"callback: {q}"); 13 | Button($"guid: {guid}", q); 14 | } 15 | 16 | [Action] 17 | async Task GuidArgumentButtonHandler(Guid guid) 18 | { 19 | Push($"value: {guid}"); 20 | await Send(); 21 | Start(); 22 | await Send(); 23 | } 24 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ActionButtonsExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.ActionButtonsExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5085", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ActionButtonsExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ActionsAndQueryExample/Deployf.Botf.ActionsAndQueryExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ActionsAndQueryExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | 3 | BotfProgram.StartBot(args); 4 | 5 | class ActionAndQueryController : BotController 6 | { 7 | [Action("/start", "start the bot")] 8 | public void Start() 9 | { 10 | PushL($"Hello!"); 11 | Push("This an example of how to use Q(...) and action's parameters"); 12 | 13 | RowButton("Simple action", Q(ActionWithNoArgs)); 14 | RowButton("Action with primitive args", Q(ActionWithPrimitiveArgs, 10, "hi")); 15 | 16 | var instance = new ExampleClass 17 | { 18 | IntField = 25, 19 | StringProp = "very looooong string with many words" 20 | }; 21 | RowButton("Action with class", Q(ActionWithStoredValue, instance)); 22 | } 23 | 24 | [Action] 25 | void ActionWithNoArgs() 26 | { 27 | Push("Just action :)"); 28 | 29 | RowButton("Back", Q(Start)); 30 | RowButton("Back(manually)", "/start"); 31 | } 32 | 33 | [Action] 34 | void ActionWithPrimitiveArgs(int arg1, string arg2) 35 | { 36 | PushL("Action with primitive arguments"); 37 | PushL($"Arg1: {arg1}"); 38 | PushL($"Arg2: {arg2}"); 39 | 40 | RowButton("Back", Q(Start)); 41 | } 42 | 43 | [Action] 44 | void ActionWithStoredValue(ExampleClass instance) 45 | { 46 | if(instance == null) 47 | { 48 | instance = new ExampleClass() 49 | { 50 | IntField = -1, 51 | StringProp = "The data was lost :( probably you had rebooted the application" 52 | }; 53 | } 54 | 55 | PushL("Action with class as a parameter"); 56 | PushL($"IntField: {instance.IntField}"); 57 | PushL($"StringProp: {instance.StringProp}"); 58 | 59 | instance.IntField += 1; 60 | var action = Q(ActionWithStoredValue, instance); 61 | RowButton("IntField += 1", action); 62 | 63 | RowButton("Back", Q(Start)); 64 | } 65 | } 66 | 67 | class ExampleClass 68 | { 69 | public int IntField; 70 | public string StringProp { get; set; } 71 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ActionsAndQueryExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.HelloExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5281", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ActionsAndQueryExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.CalendarKeyboardExample/Deployf.Botf.CalendarKeyboardExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | CS8600;CS8601;CS8602;CS8603;CS8604;CS8613;CS8614;CS8619;CS8620;CS8622;CS8625;CS8629;CS8633,CS8767 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.CalendarKeyboardExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | using Microsoft.Extensions.Logging; 3 | using System.Diagnostics; 4 | using Telegram.Bot.Types.Enums; 5 | 6 | class Program : BotfProgram 7 | { 8 | public static void Main(string[] args) => StartBot(args); 9 | 10 | readonly PagingService pagingService; 11 | readonly ILogger logger; 12 | public Program(PagingService pagingService, ILogger logger) 13 | { 14 | this.pagingService = pagingService; 15 | this.logger = logger; 16 | } 17 | 18 | [Action("/start")] 19 | public async Task Start() 20 | { 21 | await YearSelect(""); 22 | } 23 | 24 | [Action] 25 | public async Task YearSelect(string state) 26 | { 27 | PushL("Pick the time"); 28 | 29 | var now = DateTime.Now; 30 | new CalendarMessageBuilder() 31 | .Year(now.Year).Month(now.Month).Day(now.Day) 32 | .Depth(CalendarDepth.Days) 33 | .SetState(state) 34 | 35 | .OnNavigatePath(s => Q(YearSelect, s)) 36 | .OnSelectPath(d => Q(DT, d.ToBinary().Base64())) 37 | 38 | .SkipHour(d => d.Hour < 10 || d.Hour > 19) 39 | .SkipDay(d => d.DayOfWeek == DayOfWeek.Sunday || d.DayOfWeek == DayOfWeek.Saturday) 40 | .SkipMinute(d => (d.Minute % 15) != 0) 41 | .SkipYear(y => y < DateTime.Now.Year) 42 | 43 | .FormatMinute(d => $"{d:HH:mm}") 44 | .FormatText((dt, depth, b) => { 45 | b.PushL($"Select {depth}"); 46 | b.PushL($"Current state: {dt}"); 47 | }) 48 | 49 | .Build(Message, new PagingService()); 50 | 51 | await SendOrUpdate(); 52 | } 53 | 54 | [Action] 55 | public async Task DT(string dt) 56 | { 57 | var datetime = DateTime.FromBinary(dt.Base64()); 58 | Button("Select new", "/start"); 59 | Push(datetime.ToString()); 60 | await SendOrUpdate(); 61 | } 62 | 63 | [On(Handle.Unknown)] 64 | public async Task Unknown() 65 | { 66 | Reply(); 67 | await Send(Context!.GetSafeTextPayload()!); 68 | } 69 | 70 | [On(Handle.Exception)] 71 | public async Task Ex(Exception e) 72 | { 73 | logger.LogCritical(e, "Unhandled exception"); 74 | 75 | if (Context.Update.Type == UpdateType.CallbackQuery) 76 | { 77 | await AnswerCallback("Error"); 78 | } 79 | else if(Context.Update.Type == UpdateType.Message) 80 | { 81 | Push("Error"); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.CalendarKeyboardExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.CalendarKeyboardExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5085", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.CalendarKeyboardExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ChainedExample/Deployf.Botf.ChainedExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ChainedExample/Program.cs: -------------------------------------------------------------------------------- 1 | global using Deployf.Botf; 2 | using Telegram.Bot.Types.Enums; 3 | 4 | class Program : BotfProgram 5 | { 6 | public static void Main(string[] args) => StartBot(args); 7 | 8 | readonly ILogger _logger; 9 | 10 | public Program(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | [Action("Start")] 16 | [Action("/start", "start the bot")] 17 | public async Task Start() 18 | { 19 | await Send($"Hi! What is your name?"); 20 | 21 | var name = await AwaitText(() => _ = Send("Use /start to try again")); 22 | await Send($"Hi, {name}! Where are you from?"); 23 | 24 | var place = await AwaitText(); 25 | 26 | Button("Like"); 27 | Button("Don't like"); 28 | await Send($"Hi {name} from {place}! Nice to meet you!\nDo you like this place?"); 29 | 30 | var likeStatus = await AwaitQuery(); 31 | if(likeStatus == "Like") 32 | { 33 | await Send("I'm glad to heat it!\nSend /start to try it again."); 34 | } 35 | else 36 | { 37 | await Send("It's bad(\nSend /start to try it again."); 38 | } 39 | } 40 | 41 | [On(Handle.Unknown)] 42 | public async Task Unknown() 43 | { 44 | PushL("unknown"); 45 | await Send(); 46 | } 47 | 48 | [On(Handle.Exception)] 49 | public async Task Ex(Exception e) 50 | { 51 | _logger.LogCritical(e, "Unhandled exception"); 52 | 53 | if (Context.Update.Type == UpdateType.CallbackQuery) 54 | { 55 | await AnswerCallback("Error"); 56 | } 57 | else if (Context.Update.Type == UpdateType.Message) 58 | { 59 | Push("Error"); 60 | } 61 | } 62 | 63 | [On(Handle.ChainTimeout)] 64 | public void ChainTimeout() 65 | { 66 | PushL("timeout"); 67 | } 68 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ChainedExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.ChainedExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5287", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ChainedExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ControllerStateExample/Deployf.Botf.ControllerStateExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ControllerStateExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | 3 | BotfProgram.StartBot(args); 4 | 5 | class ControllerStateExample : BotController 6 | { 7 | [State] 8 | ExampleClass _data; 9 | 10 | [State] 11 | int intField; 12 | 13 | int nonStateIntField; 14 | 15 | [Action("/start", "start the bot")] 16 | public void Start() 17 | { 18 | PushL($"Hello!"); 19 | PushL("This is an example of how to store controllers state(fields and props) through updates"); 20 | PushL("Current controller state:"); 21 | DumpState(); 22 | 23 | PushL(); 24 | PushL("For refresh call /start"); 25 | 26 | RowButton("Set random _data", Q(SetRandom_data)); 27 | RowButton("Set random intField", Q(SetRandom_intField)); 28 | RowButton("Set random nonStateIntField", Q(SetRandom_nonStateIntField)); 29 | } 30 | 31 | [Action] 32 | void SetRandom_data() 33 | { 34 | _data = new ExampleClass() 35 | { 36 | IntField = Random.Shared.Next(), 37 | StringProp = Guid.NewGuid().ToString() 38 | }; 39 | Start(); 40 | } 41 | 42 | [Action] 43 | void SetRandom_intField() 44 | { 45 | intField = Random.Shared.Next(); 46 | Start(); 47 | } 48 | 49 | [Action] 50 | void SetRandom_nonStateIntField() 51 | { 52 | nonStateIntField = Random.Shared.Next(); 53 | Start(); 54 | } 55 | 56 | void DumpState() 57 | { 58 | if(_data == null) 59 | { 60 | PushL("_data is null"); 61 | } 62 | else 63 | { 64 | PushL($"_data is {_data.ToString()}"); 65 | } 66 | 67 | PushL($"intField is {intField}"); 68 | PushL($"nonStateIntField is {nonStateIntField}"); 69 | } 70 | } 71 | 72 | class ExampleClass 73 | { 74 | public int IntField; 75 | public string StringProp { get; set; } 76 | 77 | public override string ToString() => $"({IntField}, {StringProp})"; 78 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ControllerStateExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.HelloExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5281", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ControllerStateExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.GlobalStateMachineExample/Deployf.Botf.GlobalStateMachineExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.GlobalStateMachineExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | using Telegram.Bot.Types.Enums; 3 | 4 | class Program : BotfProgram 5 | { 6 | private static IServiceProvider RootProvider; 7 | public static void Main(string[] args) => StartBot(args, onRun: (app, conf) => 8 | { 9 | // need to have root provider to create scope for SetStateDeffered example 10 | RootProvider = app.ApplicationServices; 11 | }); 12 | 13 | readonly ILogger _logger; 14 | readonly IGlobalStateService _globalState; 15 | public Program(ILogger logger, IGlobalStateService globalState) 16 | { 17 | _logger = logger; 18 | _globalState = globalState; 19 | } 20 | 21 | [Action("Start")] 22 | [Action("/start", "start the bot")] 23 | public void Start() 24 | { 25 | Push($"Send `{nameof(Hello)}` to me, please!"); 26 | 27 | RowKButton(Q(State1Go)); 28 | RowKButton(Q(Hello)); 29 | RowKButton(Q(Check)); 30 | RowKButton(Q(SetStateDeffered)); 31 | } 32 | 33 | [Action("State 1")] 34 | public async Task State1Go() 35 | { 36 | await Send($"Going to state 1"); 37 | await GlobalState(new State1()); 38 | } 39 | 40 | [Action(nameof(Hello))] 41 | public void Hello() 42 | { 43 | Push("Hey! Thank you! That's it."); 44 | } 45 | 46 | [Action("Check")] 47 | public void Check() 48 | { 49 | Push($"This is main state"); 50 | } 51 | 52 | [Action("Deffered")] 53 | public void SetStateDeffered() 54 | { 55 | Push($"I will set state in 2 sec for {FromId} to {nameof(State2)}"); 56 | 57 | var _ = SetDeffered(FromId); 58 | 59 | async Task SetDeffered(long userId) 60 | { 61 | await Task.Delay(2000); 62 | 63 | var scope = RootProvider.CreateScope(); 64 | var service = scope.ServiceProvider.GetRequiredService(); 65 | await service.SetState(userId, new State2()); 66 | } 67 | } 68 | 69 | [Action("/deffered")] 70 | public async Task SetStateDefferedForUserId(long userId) 71 | { 72 | await _globalState.SetState(userId, new State2()); 73 | } 74 | 75 | [On(Handle.Unknown)] 76 | public async Task Unknown() 77 | { 78 | PushL("unknown"); 79 | await Send(); 80 | } 81 | 82 | [On(Handle.ClearState)] 83 | public void CleanState() 84 | { 85 | RowKButton(Q(State1Go)); 86 | RowKButton(Q(Hello)); 87 | RowKButton(Q(Check)); 88 | 89 | Push("Main menu"); 90 | } 91 | 92 | [On(Handle.Exception)] 93 | public async Task Ex(Exception e) 94 | { 95 | _logger.LogCritical(e, "Unhandled exception"); 96 | 97 | if (Context.Update.Type == UpdateType.CallbackQuery) 98 | { 99 | await AnswerCallback("Error"); 100 | } 101 | else if (Context.Update.Type == UpdateType.Message) 102 | { 103 | Push("Error"); 104 | } 105 | } 106 | } 107 | 108 | record State1; 109 | record State2; 110 | 111 | class State1Controller : BotControllerState 112 | { 113 | public override async ValueTask OnEnter() 114 | { 115 | RowKButton(Q(GoToMain)); 116 | RowKButton(Q(GoToState2)); 117 | RowKButton(Q(Check)); 118 | 119 | await Send("Enter State1"); 120 | } 121 | 122 | public override async ValueTask OnLeave() 123 | { 124 | await Send("Leave State1"); 125 | } 126 | 127 | [Action("Main")] 128 | public async Task GoToMain() 129 | { 130 | Push($"Going to main state"); 131 | await GlobalState(null); 132 | } 133 | 134 | [Action("State 2")] 135 | public async Task GoToState2() 136 | { 137 | Push($"Going to state 2"); 138 | await GlobalState(new State2()); 139 | } 140 | 141 | [Action("Check")] 142 | public void Check() 143 | { 144 | Push($"This is state 1"); 145 | } 146 | 147 | [On(Handle.Unknown, 1)] 148 | [Filter(Filters.CurrentGlobalState)] 149 | void UnknownForThisState() 150 | { 151 | Reply(); 152 | Push("Unknown command for State1"); 153 | Context.StopHandling(); 154 | } 155 | } 156 | 157 | class State2Controller : BotControllerState 158 | { 159 | public override async ValueTask OnEnter() 160 | { 161 | RowKButton(Q(GoToMain)); 162 | RowKButton(Q(GoToState1)); 163 | RowKButton(Q(Check)); 164 | 165 | await Send("Enter State2"); 166 | } 167 | 168 | public override async ValueTask OnLeave() 169 | { 170 | await Send("Leave State2"); 171 | } 172 | 173 | [Action("Main")] 174 | public async Task GoToMain() 175 | { 176 | Push($"Going to main state"); 177 | await GlobalState(null); 178 | } 179 | 180 | [Action("State 1")] 181 | public async Task GoToState1() 182 | { 183 | Push($"Going to state 1"); 184 | await GlobalState(new State1()); 185 | } 186 | 187 | [Action("Check")] 188 | public void Check() 189 | { 190 | Push($"This is state 2"); 191 | } 192 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.GlobalStateMachineExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.GlobalStateMachineExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5237", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.GlobalStateMachineExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.HelloExample/Deployf.Botf.HelloExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.HelloExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | 3 | class Program : BotfProgram 4 | { 5 | // It's boilerplate program entrypoint. 6 | // We just simplified all usual code into static method StartBot. 7 | // But in this case of starting of the bot, you should add a config section under "bot" key to appsettings.json 8 | public static void Main(string[] args) => StartBot(args); 9 | 10 | // Action attribute mean that you mark async method `Start` 11 | // as handler for user's text in message which equal to '/start' string. 12 | // You can name method as you want 13 | // And also, second argument of Action's attribute is a description for telegram's menu for this action 14 | [Action("/start", "start the bot")] 15 | public void Start() 16 | { 17 | // Just sending a reply message to user. Very simple, isn't? 18 | Push($"Send `{nameof(Hello)}` to me, please!"); 19 | } 20 | 21 | [Action(nameof(Hello))] 22 | public void Hello() 23 | { 24 | Push("Hey! Thank you! That's it."); 25 | } 26 | 27 | // Here we handle all unknown command or just text sent from user 28 | [On(Handle.Unknown)] 29 | public async Task Unknown() 30 | { 31 | // Here, we use the so-called "buffering of sending message" 32 | // It means you dont need to construct all message in the string and send it once 33 | // You can use Push to just add the text to result message, or PushL - the same but with new line after the string. 34 | PushL("You know.. it's very hard to recognize your command!"); 35 | PushL("Please, write a correct text. Or use /start command"); 36 | 37 | // And finally, send buffered message 38 | await Send(); 39 | } 40 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.HelloExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.HelloExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5281", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.HelloExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.MediaExample/Deployf.Botf.MediaExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.MediaExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | 3 | BotfProgram.StartBot(args); 4 | 5 | class MediaController : BotController 6 | { 7 | /* 8 | * There is a picture in the 1st message, but not in the 2nd. 9 | * What should happen: 10 | * The 1st will be deleted and the second will be sent 11 | */ 12 | 13 | #region 1st scenario 14 | 15 | [Action("/start")] 16 | void Start() 17 | { 18 | // Add the photo to message 19 | Photo("https://avatars.githubusercontent.com/u/59260433"); 20 | Push("Hello from deploy-f"); 21 | Button("Got to botf repo", Q(Test1)); 22 | } 23 | 24 | [Action] 25 | void Test1() 26 | { 27 | Push("Test1"); 28 | Button("Got to botf repo", "https://github.com/deploy-f/botf"); 29 | } 30 | 31 | #endregion 32 | 33 | 34 | /* 35 | * There is no picture in the 1st message, but there is in the 2nd. 36 | * What should happen: 37 | * The 1st will be deleted and the second will be sent 38 | */ 39 | 40 | #region 2й scenario 41 | 42 | [Action("/start2")] 43 | void Start2() 44 | { 45 | Push("Hello from deploy-f"); 46 | Button("Got to botf repo", Q(Test2)); 47 | } 48 | 49 | [Action] 50 | void Test2() 51 | { 52 | // Add the photo to message 53 | Photo("https://avatars.githubusercontent.com/u/59260433"); 54 | Push("Test2"); 55 | Button("Got to botf repo", "https://github.com/deploy-f/botf"); 56 | } 57 | 58 | #endregion 59 | 60 | 61 | /* 62 | * There is no picture in the 1st message and there is no picture in the 2nd either. 63 | * What should happen: 64 | * 1st message update via without deletion 65 | */ 66 | 67 | #region 3й scenario 68 | 69 | [Action("/start3")] 70 | void Start3() 71 | { 72 | Push("Hello from deploy-f"); 73 | Button("Got to botf repo", Q(Test3)); 74 | } 75 | 76 | [Action] 77 | void Test3() 78 | { 79 | Push("Test3"); 80 | Button("Got to botf repo", "https://github.com/deploy-f/botf"); 81 | } 82 | 83 | #endregion 84 | 85 | 86 | /* 87 | * In the 1st message there is a picture and in the 2nd there is. 88 | * What should happen: 89 | * The 1st message will update its text and image without deleting. 90 | * If you specify UpdateMessagePolicy to DeleteAndSend, so the 1st message will be deleted 91 | * and new message with new picture will be sent 92 | * Note from Telegram: When an inline message is edited, a new file can't be uploaded; 93 | * use a previously uploaded file via its file_id or specify a URL. 94 | */ 95 | 96 | #region 4й scenario 97 | 98 | [Action("/start4")] 99 | void Start4() 100 | { 101 | // Add the photo to message 102 | Photo("https://avatars.githubusercontent.com/u/59260433"); 103 | Push("Hello from deploy-f"); 104 | Button("Got to botf repo", Q(Test4)); 105 | } 106 | 107 | [Action] 108 | void Test4() 109 | { 110 | Context.SetUpdateMsgPolicy(UpdateMessagePolicy.UpdateContent); 111 | // Add a new photo to message 112 | Photo("https://icons-for-free.com/iconfiles/png/512/csharp+line-1324760527290176528.png"); 113 | Push("Test4"); 114 | Button("Got to botf repo", "https://github.com/deploy-f/botf"); 115 | } 116 | 117 | #endregion 118 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.MediaExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.MediaExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5085", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.MediaExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.PingExample/Deployf.Botf.PingExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.PingExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | 3 | class Program : BotfProgram 4 | { 5 | public static void Main(string[] args) => StartBot(args); 6 | 7 | [On(Handle.Unknown)] 8 | public async Task Unknown() 9 | { 10 | Reply(); 11 | await Send(Context.GetSafeTextPayload()!); 12 | } 13 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.PingExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:44742", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "Deployf.Botf.PingExample": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": false, 15 | "applicationUrl": "http://localhost:5203", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": false, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.PingExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ReminderBot/Deployf.Botf.ReminderBot.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | CS8600;CS8601;CS8602;CS8603;CS8604;CS8613;CS8614;CS8619;CS8620;CS8622;CS8625;CS8629;CS8633,CS8767 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ReminderBot/MainController.cs: -------------------------------------------------------------------------------- 1 | using SQLite; 2 | using Telegram.Bot.Types.Enums; 3 | 4 | namespace Deployf.Botf.ScheduleExample; 5 | 6 | class MainController : BotController 7 | { 8 | readonly TableQuery _users; 9 | readonly SQLiteConnection _db; 10 | readonly ILogger _logger; 11 | readonly BotfOptions _options; 12 | 13 | static IReadOnlyCollection _timeZones = TimeZoneInfo.GetSystemTimeZones(); 14 | 15 | public MainController(TableQuery users, SQLiteConnection db, ILogger logger, BotfOptions options) 16 | { 17 | _users = users; 18 | _db = db; 19 | _logger = logger; 20 | _options = options; 21 | } 22 | 23 | 24 | [Action("/start", "start the bot")] 25 | public void Start() 26 | { 27 | PushL("Hello!"); 28 | PushL("This bot allow you adding a recurring reminder to chat"); 29 | PushL(); 30 | RowButton("Add a reminder", Q(c => c.Add)); 31 | RowButton("List reminders", Q(c => c.List)); 32 | RowButton("Set your timezone", Q(ListTimezonesCmd)); 33 | } 34 | 35 | [Action("/timezone")] 36 | public void ListTimezonesCmd() 37 | { 38 | ListTimezones(0); 39 | } 40 | 41 | [Action] 42 | public void ListTimezones(int page) 43 | { 44 | var user =_users.First(c => c.Id == FromId); 45 | Push($"Current timezone: {user.TimeZone}"); 46 | 47 | var pager = new PagingService(); 48 | var query = _timeZones.Select((c, i) => new { zone = c, index = i }).AsQueryable(); 49 | var pageModel = pager.Paging(query, new PageFilter { Count = 10, Page = page }); 50 | Pager(pageModel, i => (i.zone.DisplayName, Q(SetTimezone, i.index)), Q(ListTimezones, "{0}"), 1); 51 | } 52 | 53 | [Action] 54 | public void SetTimezone(int zone) 55 | { 56 | var user = _users.First(c => c.Id == FromId); 57 | user.TimeZone = _timeZones.ElementAt(zone).Id; 58 | _db.Update(user); 59 | 60 | Push("Timezone has been setted"); 61 | Button("Back to main menu", Q(Start)); 62 | } 63 | 64 | 65 | // if user sent unknown action, say it to them 66 | [On(Handle.Unknown)] 67 | public void Unknown() 68 | { 69 | Push("Unknown command. Or use /start command"); 70 | } 71 | 72 | // handle all messages before botf has processed it 73 | // and yes, action methods can be void 74 | [On(Handle.BeforeAll)] 75 | public void PreHandle() 76 | { 77 | // if user has never contacted with the bot we add them to our db at first time 78 | if(!_users.Any(c => c.Id == FromId)) 79 | { 80 | var user = new User 81 | { 82 | Id = FromId, 83 | FullName = Context!.GetUserFullName(), 84 | Username = Context!.GetUsername()!, 85 | TimeZone = TimeZoneInfo.Local.Id 86 | }; 87 | _db.Insert(user); 88 | _logger.LogInformation("Added user {tgId} at first time", user.Id); 89 | } 90 | } 91 | 92 | // handle all errors while message are processing 93 | [On(Handle.Exception)] 94 | public async Task OnException(Exception e) 95 | { 96 | _logger.LogError(e, "Unhandled exception"); 97 | if (Context.Update.Type == UpdateType.CallbackQuery) 98 | { 99 | await AnswerCallback("Error"); 100 | } 101 | else if (Context.Update.Type == UpdateType.Message) 102 | { 103 | Push("Error"); 104 | } 105 | } 106 | 107 | // we'll handle auth error if user without roles try use action marked with [Authorize("policy")] 108 | [On(Handle.Unauthorized)] 109 | public void Forbidden() 110 | { 111 | Push("Forbidden!"); 112 | } 113 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ReminderBot/Models.cs: -------------------------------------------------------------------------------- 1 | using SQLite; 2 | 3 | public class Reminder 4 | { 5 | [PrimaryKey, AutoIncrement] 6 | public int Id { get; set; } 7 | 8 | [Indexed] 9 | public long ChatId { get; set; } 10 | 11 | [Indexed] 12 | public DateTime Time { get; set; } 13 | 14 | public WeekDay Repeating { get; set; } 15 | 16 | public string? Comment { get; set; } 17 | } 18 | 19 | public class User 20 | { 21 | [PrimaryKey] 22 | public long Id { get; set; } 23 | 24 | [Indexed] 25 | public string Username { get; set; } = String.Empty; 26 | 27 | public string FullName { get; set; } = String.Empty; 28 | 29 | public string TimeZone { get; set; } = String.Empty; 30 | } 31 | 32 | public enum WeekDay 33 | { 34 | Sunday = 1, 35 | Monday = 1 << 1, 36 | Tuesday = 1 << 2, 37 | Wednesday = 1 << 3, 38 | Thursday = 1 << 4, 39 | Friday = 1 << 5, 40 | Saturday = 1 << 6, 41 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ReminderBot/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | using Hangfire; 3 | using Hangfire.Storage.SQLite; 4 | using SQLite; 5 | 6 | BotfProgram.StartBot(args, onConfigure: (svc, cfg) => 7 | { 8 | var db = new SQLiteConnection("db.sqlite"); 9 | 10 | db.CreateTable(); 11 | db.CreateTable(); 12 | 13 | 14 | svc.AddSingleton(db.Table()); 15 | svc.AddSingleton(db.Table()); 16 | svc.AddSingleton(db); 17 | svc.AddSingleton(); 18 | 19 | svc.AddHangfire(c => c.SetDataCompatibilityLevel(CompatibilityLevel.Version_170) 20 | .UseColouredConsoleLogProvider() 21 | .UseSimpleAssemblyNameTypeSerializer() 22 | .UseRecommendedSerializerSettings() 23 | .UseSQLiteStorage()); 24 | svc.AddHangfireServer(); 25 | 26 | }, onRun: (app, cfg) => 27 | { 28 | app.UseHangfireDashboard(); 29 | }); 30 | 31 | class HangfireActivator : JobActivator 32 | { 33 | readonly IServiceProvider _serviceProvider; 34 | public HangfireActivator(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; 35 | public override object? ActivateJob(Type type) => _serviceProvider.GetService(type); 36 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ReminderBot/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.ReminderBot": { 4 | "commandName": "Project", 5 | "hotReloadProfile": "aspnetcore", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "applicationUrl": "http://localhost:5043", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ReminderBot/ReminderJob.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf.ScheduleExample; 2 | 3 | public class ReminderJob 4 | { 5 | readonly MessageBuilder Message; 6 | readonly MessageSender Sender; 7 | readonly ILogger Log; 8 | 9 | public ReminderJob(MessageSender sender, ILogger log) 10 | { 11 | Message = new MessageBuilder(); 12 | Sender = sender; 13 | Log = log; 14 | } 15 | 16 | public async Task Exec(Reminder reminder) 17 | { 18 | try 19 | { 20 | Message.Push(reminder.Comment!); 21 | Message.SetChatId(reminder.ChatId); 22 | await Sender.Send(Message); 23 | } 24 | catch(Exception ex) 25 | { 26 | Log.LogError(ex, "Exception in ReminderJob"); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ReminderBot/UserService.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | using SQLite; 3 | 4 | public class UserService : IBotUserService 5 | { 6 | readonly TableQuery _users; 7 | static string[] _zeroRoles = new string[0]; 8 | 9 | public UserService(TableQuery users) 10 | { 11 | _users = users; 12 | } 13 | 14 | public ValueTask<(string? id, string[]? roles)> GetUserIdWithRoles(long tgUserId) 15 | { 16 | var user = _users.FirstOrDefault(c => c.Id == tgUserId); 17 | if (user == null) 18 | { 19 | return new ((null, null)); 20 | } 21 | 22 | 23 | var id = user.Id.ToString(); 24 | return new ((id, _zeroRoles)); 25 | } 26 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ReminderBot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/AdminController.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | using SQLite; 3 | 4 | class AdminController : BotController 5 | { 6 | readonly TableQuery _users; 7 | readonly SQLiteConnection _db; 8 | 9 | public AdminController(TableQuery users, SQLiteConnection db) 10 | { 11 | _users = users; 12 | _db = db; 13 | } 14 | 15 | // it needs to avoid any configuration for specifying admins 16 | // it's difficult process, you need to find your telegram's user id 17 | // but this way allow us to became admin without any configuration. 18 | // you need just call this command in telegram at first, and we're admin now 19 | [Action("/i_am_admin")] 20 | public async Task IAmAdmin() 21 | { 22 | if(_users.Any(c => (c.Roles & UserRole.admin) == UserRole.admin)) 23 | { 24 | await Send("No-no-no. You can't do it!"); 25 | return; 26 | } 27 | 28 | var user = _users.FirstOrDefault(c => c.Id == FromId); 29 | user.Roles = user.Roles | UserRole.admin; // just set new role with bitflag feature in c# 30 | _db.Update(user); 31 | 32 | Push($"You are admin now!"); 33 | } 34 | 35 | // this command only for check, am i admin now or not. 36 | // Authorize attribute will do all work for ass 37 | [Action("/am_i_admin")] 38 | [Authorize("admin")] 39 | public void AmIAdmin() 40 | { 41 | Push($"You are admin"); 42 | } 43 | 44 | [Action("/set_scheduler")] 45 | [Authorize("admin")] 46 | public void SetScheduler() 47 | { 48 | State(new SetSchedulerState()); 49 | Push($"Text me telegram user's username:"); 50 | } 51 | 52 | [State] 53 | public void HandleSchedulerState(SetSchedulerState _) 54 | { 55 | var payload = Context!.GetSafeTextPayload(); 56 | var user = _users.FirstOrDefault(c => c.Username == payload); 57 | if(user == null) 58 | { 59 | Push("User not found"); 60 | return; 61 | } 62 | user.Roles = user.Roles | UserRole.scheduler; 63 | _db.Update(user); 64 | Push("User became an scheduler"); 65 | } 66 | public record SetSchedulerState(); 67 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/Deployf.Botf.ScheduleExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | CS8600;CS8601;CS8602;CS8603;CS8604;CS8613;CS8614;CS8619;CS8620;CS8622;CS8625;CS8629;CS8633,CS8767 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/MainController.cs: -------------------------------------------------------------------------------- 1 | using SQLite; 2 | using Telegram.Bot.Types.Enums; 3 | 4 | namespace Deployf.Botf.ScheduleExample; 5 | 6 | class MainController : BotController 7 | { 8 | readonly TableQuery _users; 9 | readonly SQLiteConnection _db; 10 | readonly ILogger _logger; 11 | readonly BotfOptions _options; 12 | 13 | public MainController(TableQuery users, SQLiteConnection db, ILogger logger, BotfOptions options) 14 | { 15 | _users = users; 16 | _db = db; 17 | _logger = logger; 18 | _options = options; 19 | } 20 | 21 | 22 | [Action("/start", "start the bot")] 23 | public void Start() 24 | { 25 | PushL("Hello!"); 26 | PushL("This bot allow you and users to book the time slot"); 27 | PushL(); 28 | PushL($"Link to book your free slots: https://t.me/{_options.Username}?start={FromId.Base64()}"); 29 | } 30 | 31 | 32 | // if user sent unknown action, say it to them 33 | [On(Handle.Unknown)] 34 | public void Unknown() 35 | { 36 | Push("Unknown command. Or use /start command"); 37 | } 38 | 39 | // handle all messages before botf has processed it 40 | // and yes, action methods can be void 41 | [On(Handle.BeforeAll)] 42 | public void PreHandle() 43 | { 44 | // if user has never contacted with the bot we add them to our db at first time 45 | if(!_users.Any(c => c.Id == FromId)) 46 | { 47 | var user = new User 48 | { 49 | Id = FromId, 50 | FullName = Context!.GetUserFullName(), 51 | Username = Context!.GetUsername()!, 52 | Roles = UserRole.scheduler 53 | }; 54 | _db.Insert(user); 55 | _logger.LogInformation("Added user {tgId} at first time", user.Id); 56 | } 57 | } 58 | 59 | // handle all errors while message are processing 60 | [On(Handle.Exception)] 61 | public async Task OnException(Exception e) 62 | { 63 | _logger.LogError(e, "Unhandled exception"); 64 | if (Context.Update.Type == UpdateType.CallbackQuery) 65 | { 66 | await AnswerCallback("Error"); 67 | } 68 | else if (Context.Update.Type == UpdateType.Message) 69 | { 70 | Push("Error"); 71 | } 72 | } 73 | 74 | // we'll handle auth error if user without roles try use action marked with [Authorize("policy")] 75 | [On(Handle.Unauthorized)] 76 | public void Forbidden() 77 | { 78 | Push("Forbidden!"); 79 | } 80 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/Models.cs: -------------------------------------------------------------------------------- 1 | using SQLite; 2 | 3 | public class Schedule 4 | { 5 | [PrimaryKey, AutoIncrement] 6 | public int Id { get; set; } 7 | 8 | [Indexed] 9 | public long OwnerId { get; set; } 10 | 11 | [Indexed] 12 | public long? UserId { get; set; } 13 | 14 | [Indexed] 15 | public DateTime From { get; set; } 16 | [Indexed] 17 | public DateTime To { get; set; } 18 | 19 | [Indexed] 20 | public State State { get; set; } 21 | 22 | public string? Comment { get; set; } 23 | } 24 | 25 | public class User 26 | { 27 | [PrimaryKey] 28 | public long Id { get; set; } 29 | 30 | [Indexed] 31 | public string Username { get; set; } = String.Empty; 32 | 33 | public string FullName { get; set; } = String.Empty; 34 | 35 | public UserRole Roles { get; set; } 36 | public string? Timezone { get; set; } 37 | } 38 | 39 | public enum State 40 | { 41 | Free, 42 | Requested, 43 | Booked, 44 | Canceled, 45 | } 46 | 47 | [Flags] 48 | public enum UserRole 49 | { 50 | none = 0, 51 | admin = 1, 52 | scheduler = 2 53 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | using SQLite; 3 | 4 | BotfProgram.StartBot(args, onConfigure: (svc, cfg) => 5 | { 6 | var db = new SQLiteConnection("db.sqlite"); 7 | 8 | db.CreateTable(); 9 | db.CreateTable(); 10 | 11 | svc.AddSingleton(db.Table()); 12 | svc.AddSingleton(db.Table()); 13 | svc.AddSingleton(db); 14 | svc.AddTransient(); 15 | svc.AddSingleton(); 16 | }); -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.ScheduleExample": { 4 | "commandName": "Project", 5 | "hotReloadProfile": "aspnetcore", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "applicationUrl": "http://localhost:5043", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/SlotController.Adding.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf.ScheduleExample; 2 | 3 | /// Part of adding single part 4 | partial class SlotController 5 | { 6 | [Action] 7 | void AddSlotFrom(string state_from) 8 | { 9 | var now = DateTime.Now; 10 | new CalendarMessageBuilder() 11 | .Year(now.Year).Month(now.Month) 12 | .Depth(CalendarDepth.Minutes) 13 | .SetState(state_from) 14 | 15 | .OnNavigatePath(s => Q(AddSlotFrom, s)) 16 | .OnSelectPath(d => Q(AddSlotTo, d.ToBinary().Base64(), ".")) 17 | 18 | .SkipTo(now) 19 | 20 | .FormatMinute(d => $"{d:HH:mm}") 21 | .FormatText((dt, depth, b) => 22 | { 23 | var selection = depth switch 24 | { 25 | CalendarDepth.Years => "year", 26 | CalendarDepth.Months => "month", 27 | CalendarDepth.Days => "day", 28 | CalendarDepth.Hours => "hour", 29 | CalendarDepth.Minutes => "minute", 30 | _ => throw new NotImplementedException() 31 | }; 32 | b.Push($"Select {selection} of the from date"); 33 | }) 34 | 35 | .Build(Message, new PagingService()); 36 | } 37 | 38 | [Action] 39 | void AddSlotTo(string dt_from, string state_to) 40 | { 41 | var from = DateTime.FromBinary(dt_from.Base64()); 42 | new CalendarMessageBuilder() 43 | .Year(from.Year).Month(from.Month) 44 | .Depth(CalendarDepth.Minutes) 45 | .SetState(state_to) 46 | 47 | .OnNavigatePath(s => Q(AddSlotTo, dt_from, s)) 48 | .OnSelectPath(d => Q(LetsAddSlot, dt_from, d.ToBinary().Base64())) 49 | 50 | .SkipTo(from) 51 | 52 | .FormatMinute(d => $"{d:HH:mm}") 53 | .FormatText((dt, depth, b) => 54 | { 55 | b.PushL($"✅The from date is {from}"); 56 | 57 | var selection = depth switch 58 | { 59 | CalendarDepth.Years => "year", 60 | CalendarDepth.Months => "month", 61 | CalendarDepth.Days => "day", 62 | CalendarDepth.Hours => "hour", 63 | CalendarDepth.Minutes => "minute", 64 | _ => throw new NotImplementedException() 65 | }; 66 | b.Push($"Select {selection} of the to date"); 67 | }) 68 | 69 | .Build(Message, new PagingService()); 70 | } 71 | 72 | [Action] 73 | async Task LetsAddSlot(string dt_from, string dt_to) 74 | { 75 | var from = DateTime.FromBinary(dt_from.Base64()); 76 | var to = DateTime.FromBinary(dt_to.Base64()); 77 | var schedule = new Schedule 78 | { 79 | OwnerId = FromId, 80 | State = global::State.Free, 81 | From = from, 82 | To = to 83 | }; 84 | await service.Add(schedule); 85 | 86 | PushL("Slot has been added"); 87 | 88 | SlotView(schedule.Id); 89 | } 90 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/SlotController.Admin.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf.ScheduleExample; 2 | 3 | /// Admin part of slot management 4 | partial class SlotController 5 | { 6 | [Action("/list_all"), Authorize("admin")] 7 | async Task ListSchedulers() 8 | { 9 | await ListSchedulersPage(0); 10 | } 11 | 12 | [Action, Authorize("admin")] 13 | async Task ListSchedulersPage(int page = 0) 14 | { 15 | PushL("Schedulers:"); 16 | var pager = await service.GetSchedulers(new PageFilter { Page = page }); 17 | Pager(pager, 18 | u => (u.FullName, Q(ListCalendarDays, u.Id.Base64(), 0)), 19 | Q(ListSchedulersPage, "{0}"), 20 | 1 21 | ); 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/SlotController.Filling.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Deployf.Botf.ScheduleExample; 4 | 5 | /// Filling part of slots 6 | partial class SlotController : BotController 7 | { 8 | [Action] 9 | void FillCalendar([State] FillState state) 10 | { 11 | PushL("Fill periodically time slot"); 12 | 13 | RowButton(state.Start.HasValue ? $"From {state.Start:HH:mm}" : "Set start time", Q(Fill_LoopStart, ".")); 14 | RowButton(state.End.HasValue ? $"To {state.End:HH:mm}" : "Set finish time", Q(Fill_LoopFinish, ".", "")); 15 | RowButton(state.UpTo.HasValue ? $"Series up to {state.UpTo:MM.dd}" : "Set series end date", Q(Fill_LoopUpTo, ".")); 16 | RowButton(state.WeekDays.HasValue ? $"Repeating {WeekdaysString(state.WeekDays.Value)}" : "Set repeating", Q(Fill_LoopWeekDays, 0)); 17 | RowButton(string.IsNullOrEmpty(state.Comment) ? "Set or update comment" : "Comment: " + state.Comment, Q(Fill_Comment)); 18 | 19 | if (state.IsSet) 20 | { 21 | RowButton("Schedule", Q(Fill, "")); 22 | } 23 | } 24 | 25 | [Action] 26 | void Fill_LoopStart(string state) 27 | { 28 | Push("Peek starts time for a slot"); 29 | 30 | var now = DateTime.Now; 31 | 32 | Calendar().Depth(CalendarDepth.Time) 33 | .SetState(state) 34 | 35 | .OnNavigatePath(s => Q(Fill_LoopStart, s)) 36 | .OnSelectPath((d, s) => Q(Fill_SetStart, d, "")) 37 | .Build(Message); 38 | } 39 | 40 | [Action] 41 | async ValueTask Fill_SetStart(DateTime start, [State] FillState state) 42 | {; 43 | state = state with { Start = start }; 44 | await AState(state); 45 | FillCalendar(state); 46 | } 47 | 48 | 49 | [Action] 50 | void Fill_LoopFinish(string state, [State] FillState fillState) 51 | { 52 | Push("Peek finish time for a slot"); 53 | 54 | var now = DateTime.Now; 55 | Calendar().Depth(CalendarDepth.Time) 56 | .SkipTo(fillState.Start.GetValueOrDefault(DateTime.Now)) 57 | .SetState(state) 58 | 59 | .OnNavigatePath(s => Q(Fill_LoopFinish, s, "")) 60 | .OnSelectPath((d, s) => Q(Fill_SetFinish, d, "")) 61 | .Build(Message); 62 | } 63 | 64 | [Action] 65 | async ValueTask Fill_SetFinish(DateTime finish, [State] FillState state) 66 | { 67 | state = state with { End = finish }; 68 | await AState(state); 69 | FillCalendar(state); 70 | } 71 | 72 | 73 | 74 | [Action] 75 | void Fill_LoopUpTo(string state) 76 | { 77 | Push("Peek to day"); 78 | 79 | var now = DateTime.Now; 80 | Calendar().Day(null).Depth(CalendarDepth.Date) 81 | .SkipTo(now) 82 | .SetState(state) 83 | 84 | .OnNavigatePath(s => Q(Fill_LoopUpTo, s)) 85 | .OnSelectPath((d, s) => Q(Fill_SetUpTo, d, "")) 86 | .Build(Message); 87 | } 88 | 89 | [Action] 90 | async ValueTask Fill_SetUpTo(DateTime upTo, [State] FillState state) 91 | { 92 | state = state with { UpTo = upTo }; 93 | await AState(state); 94 | FillCalendar(state); 95 | } 96 | 97 | 98 | [Action] 99 | void Fill_LoopWeekDays(WeekDay weekdays) 100 | { 101 | Push("Peek weekdays"); 102 | 103 | new FlagMessageBuilder(weekdays) 104 | .Navigation(s => Q(Fill_LoopWeekDays, s)) 105 | .Build(Message); 106 | 107 | RowButton("Done", Q(Fill_SetWeekDays, weekdays, "")); 108 | } 109 | 110 | [Action] 111 | async ValueTask Fill_SetWeekDays(WeekDay weekdays, [State] FillState state) 112 | { 113 | state = state with { WeekDays = weekdays }; 114 | await AState(state); 115 | FillCalendar(state); 116 | } 117 | 118 | [Action] 119 | void Fill_Comment() 120 | { 121 | State(new SetCommentState()); 122 | Push("Send a comment"); 123 | } 124 | 125 | [State] 126 | async ValueTask Fill_StateComment(SetCommentState state) 127 | { 128 | var fillState = await GetAState(); 129 | fillState = fillState with { Comment = Context.GetSafeTextPayload() }; 130 | await AState(state); 131 | FillCalendar(fillState); 132 | } 133 | record SetCommentState; 134 | 135 | 136 | [Action] 137 | async ValueTask Fill([State] FillState state) 138 | { 139 | await service.AddSeries(new(FromId, 140 | DateTime.Now.Date, 141 | state.UpTo!.Value, 142 | state.WeekDays!.Value, 143 | state.Start!.Value, 144 | state.End!.Value)); 145 | 146 | 147 | PushL("✅ added"); 148 | await ListCalendarDays(FromId.Base64(), 0); 149 | 150 | await AnswerCallback("✅ added"); 151 | } 152 | 153 | 154 | struct FillState 155 | { 156 | public DateTime? Start { get; set; } 157 | public DateTime? End { get; set; } 158 | public DateTime? UpTo { get; set; } 159 | public WeekDay? WeekDays { get; set; } 160 | public string? Comment { get; set; } 161 | 162 | public bool IsSet => Start.HasValue 163 | && End.HasValue 164 | && UpTo.HasValue 165 | && WeekDays.HasValue 166 | && !string.IsNullOrEmpty(Comment); 167 | } 168 | 169 | string WeekdaysString(WeekDay weekDay) 170 | { 171 | var values = Enum.GetValues(); 172 | var result = string.Empty; 173 | if (weekDay == 0) 174 | { 175 | return "not set"; 176 | } 177 | 178 | for (int i = 0; i < values.Length; i++) 179 | { 180 | if ((weekDay & values[i]) == values[i]) 181 | { 182 | result += values[i].ToString()[0..2] + " "; 183 | } 184 | } 185 | 186 | return result; 187 | } 188 | 189 | CalendarMessageBuilder Calendar() 190 | { 191 | var now = DateTime.Now; 192 | return new CalendarMessageBuilder() 193 | .Year(now.Year) 194 | .Month(now.Month) 195 | .Day(now.Day); 196 | //.Culture(CultureInfo.GetCultureInfo("uk-UA")); 197 | } 198 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/SlotController.Views.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf.ScheduleExample; 2 | 3 | /// Slot views and actions for manage the slot 4 | partial class SlotController 5 | { 6 | [Action] 7 | async Task ListCalendarDays(string uid64, int page) 8 | { 9 | var now = DateTime.Now.Date; 10 | var pager = await service.GetFreeDays(uid64.Base64(), now, new PageFilter { Page = page }); 11 | 12 | if (pager.Count == 0) 13 | { 14 | Push("Nothing :("); 15 | } 16 | else 17 | { 18 | Push("Peek a day:"); 19 | } 20 | 21 | Pager(pager, 22 | u => ($"{u:dd.MM.yyyy}", Q(ListFreeSlots, uid64, u.ToBinary().Base64(), 0)), 23 | Q(ListCalendarDays, uid64, "{0}") 24 | ); 25 | } 26 | 27 | [Action] 28 | async Task ListFreeSlots(string uid64, string dt64, int page) 29 | { 30 | PushL("Peek free slot:"); 31 | 32 | var user = uid64.Base64(); 33 | var date = DateTime.FromBinary(dt64.Base64()); 34 | var pager = await service.GetFreeSlots(user, date, new PageFilter { Page = page }); 35 | 36 | Pager(pager, 37 | u => ($"{u.From:HH.mm} - {u.To:HH.mm}", Q(SlotView, u.Id)), 38 | Q(ListFreeSlots, uid64, dt64, "{0}"), 39 | 3 40 | ); 41 | 42 | if (pager.Count > 0 && (user == FromId || User.IsInRole("admin"))) 43 | { 44 | RowButton("Cancel this day", Q(ApproveCancelDay, uid64, dt64)); 45 | } 46 | 47 | RowButton("🔙 Back", Q(ListCalendarDays, uid64, 0)); 48 | } 49 | 50 | [Action] 51 | void SlotView(int scheduleId) 52 | { 53 | var schedule = service.Get(scheduleId); 54 | 55 | PushL($"{schedule.State} time slot"); 56 | 57 | PushL($"Date: {schedule.From:dd.MM.yyyy}"); 58 | PushL($"Time: {schedule.From:HH:mm} - {schedule.To:HH:mm}"); 59 | if (!string.IsNullOrEmpty(schedule.Comment)) 60 | { 61 | PushL(); 62 | Push(schedule.Comment); 63 | } 64 | 65 | if (FromId == schedule.OwnerId) 66 | { 67 | if (schedule.State != global::State.Canceled) 68 | { 69 | RowButton("Cancel", Q(Cancel, scheduleId)); 70 | } 71 | if (schedule.State == global::State.Canceled) 72 | { 73 | RowButton("Free", Q(Free, scheduleId)); 74 | } 75 | } 76 | else 77 | { 78 | RowButton("Book", Q(Book, scheduleId)); 79 | } 80 | 81 | RowButton("🔙 Back", Q(ListFreeSlots, schedule.OwnerId.Base64(), schedule.From.Date.ToBinary().Base64(), 0)); 82 | } 83 | 84 | [Action] 85 | async Task Book(int scheduleId) 86 | { 87 | await service.Book(scheduleId); 88 | PushL("Booked"); 89 | SlotView(scheduleId); 90 | } 91 | 92 | [Action] 93 | async Task Cancel(int scheduleId) 94 | { 95 | await service.Cancel(scheduleId); 96 | PushL("Canceled"); 97 | SlotView(scheduleId); 98 | } 99 | 100 | [Action] 101 | void ApproveCancelDay(string uid64, string dt64) 102 | { 103 | Push("Are you sure?"); 104 | Button("Yes, cancel it", Q(CancelDay, uid64, dt64)); 105 | Button("No, go back", Q(ListFreeSlots, uid64, dt64, 0)); 106 | } 107 | 108 | [Action] 109 | async Task CancelDay(string uid64, string dt64) 110 | { 111 | var user = uid64.Base64(); 112 | if (FromId == user || User.IsInRole("admin")) 113 | { 114 | var date = DateTime.FromBinary(dt64.Base64()); 115 | await service.CancelDay(user, date); 116 | PushL("Canceled!"); 117 | await AnswerCallback("Canceled"); 118 | } 119 | } 120 | 121 | [Action] 122 | async Task Free(int scheduleId) 123 | { 124 | await service.Free(scheduleId); 125 | PushL("Now it is free"); 126 | SlotView(scheduleId); 127 | } 128 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/SlotController.cs: -------------------------------------------------------------------------------- 1 | namespace Deployf.Botf.ScheduleExample; 2 | 3 | /// 4 | /// Entypoint to slot commands and management 5 | /// 6 | public partial class SlotController 7 | { 8 | readonly SlotService service; 9 | 10 | public SlotController(SlotService service) 11 | { 12 | this.service = service; 13 | } 14 | 15 | [Action("/start")] 16 | async Task StartDeeplink(string uid) 17 | { 18 | await ListCalendarDays(uid, 0); 19 | } 20 | 21 | [Action("/list", "show my slots")] 22 | async Task ListMySlots() 23 | { 24 | await ListCalendarDays(FromId.Base64(), 0); 25 | } 26 | 27 | [Action("/fill", "add a serries slots")] 28 | async ValueTask FillCommand() 29 | { 30 | FillCalendar(await GetAState()); 31 | } 32 | 33 | [Action("/add", "add the time free slot")] 34 | void AddSlot() 35 | { 36 | AddSlotFrom("."); 37 | } 38 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/SlotService.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | using SQLite; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | public class SlotService 8 | { 9 | readonly TableQuery _repo; 10 | readonly TableQuery _users; 11 | readonly SQLiteConnection _db; 12 | readonly PagingService _paging; 13 | readonly MessageSender _sender; 14 | 15 | public SlotService(TableQuery repo, SQLiteConnection db, PagingService paging, MessageSender sender, TableQuery users) 16 | { 17 | _repo = repo; 18 | _db = db; 19 | _paging = paging; 20 | _sender = sender; 21 | _users = users; 22 | } 23 | 24 | public Paging Get(ScheduleFilter filter) 25 | { 26 | var query = _repo.AsQueryable(); 27 | 28 | if(filter.State != null) 29 | { 30 | query = query.Where(c => c.State == filter.State); 31 | } 32 | 33 | if(filter.OwnerId != null) 34 | { 35 | query = query.Where(c => c.OwnerId == filter.OwnerId); 36 | } 37 | 38 | if (filter.UserId != null) 39 | { 40 | query = query.Where(c => c.UserId == filter.UserId); 41 | } 42 | 43 | if(filter.From != null) 44 | { 45 | query = query.Where(c => c.From > filter.From); 46 | } 47 | 48 | if (filter.To != null) 49 | { 50 | query = query.Where(c => c.To < filter.To); 51 | } 52 | 53 | return _paging.Paging(query, filter); 54 | } 55 | 56 | public Schedule Get(int id) 57 | { 58 | return _repo.FirstOrDefault(c => c.Id == id); 59 | } 60 | 61 | public async ValueTask Update(Schedule schedule) 62 | { 63 | _db.Update(schedule); 64 | 65 | if (schedule.UserId != null) 66 | { 67 | var msg = new MessageBuilder() 68 | .SetChatId(schedule.UserId.Value) 69 | .Push("Schedule has changed"); //todo: replace text 70 | await _sender.Send(msg); 71 | } 72 | } 73 | 74 | public ValueTask Add(Schedule slot) 75 | { 76 | _db.Insert(slot); 77 | return ValueTask.CompletedTask; 78 | } 79 | 80 | public async ValueTask AddSeries(CreateSeriesParams param) 81 | { 82 | for(var day = param.SeriesFrom; day <= param.SeriesTo; day = day.AddDays(1)) 83 | { 84 | var date = day.Date; 85 | var from = date.AddHours(param.Start.Hour).AddMinutes(param.Start.Minute); 86 | var to = date.AddHours(param.To.Hour).AddMinutes(param.To.Minute); 87 | 88 | var weekday = Enum.Parse(day.DayOfWeek.ToString()); 89 | 90 | if(!param.WeekDays.HasFlag(weekday)) 91 | { 92 | continue; 93 | } 94 | 95 | var slot = new Schedule 96 | { 97 | OwnerId = param.OwnerId, 98 | State = State.Free, 99 | From = from, 100 | To = to 101 | }; 102 | await Add(slot); 103 | } 104 | } 105 | 106 | public async ValueTask Book(int scheduleId) 107 | { 108 | var model = _repo.First(c => c.Id == scheduleId); 109 | model.State = State.Requested; 110 | _db.Update(model); 111 | 112 | if (model.OwnerId != 0) 113 | { 114 | var msg = new MessageBuilder() 115 | .SetChatId(model.OwnerId) 116 | .Push("Book requested"); //todo: replace text 117 | await _sender.Send(msg); 118 | } 119 | 120 | return model; 121 | } 122 | 123 | public async ValueTask Reject(int scheduleId) 124 | { 125 | var model = _repo.First(c => c.Id == scheduleId); 126 | model.State = State.Free; 127 | _db.Update(model); 128 | 129 | if (model.UserId != null) 130 | { 131 | var msg = new MessageBuilder() 132 | .SetChatId(model.UserId.Value) 133 | .Push("Your booking was rejected"); //todo: replace text 134 | await _sender.Send(msg); 135 | } 136 | 137 | return model; 138 | } 139 | 140 | public async ValueTask Approve(int scheduleId) 141 | { 142 | var model = _repo.First(c => c.Id == scheduleId); 143 | model.State = State.Booked; 144 | _db.Update(model); 145 | 146 | if (model.UserId != null) 147 | { 148 | var msg = new MessageBuilder() 149 | .SetChatId(model.UserId.Value) 150 | .Push("Your booking was approved"); //todo: replace text 151 | await _sender.Send(msg); 152 | } 153 | 154 | return model; 155 | } 156 | 157 | public async ValueTask CancelDay(long ownerId, DateTime date) 158 | { 159 | var day = date.Date; 160 | var nextDay = day.AddDays(1); 161 | var slots = _repo.Where(c => c.OwnerId == ownerId && c.From >= day && c.From <= nextDay); 162 | foreach (var slot in slots) 163 | { 164 | await Cancel(slot); 165 | } 166 | } 167 | 168 | public async ValueTask Cancel(int scheduleId) 169 | { 170 | var model = _repo.First(c => c.Id == scheduleId); 171 | return await Cancel(model); 172 | } 173 | 174 | private async ValueTask Cancel(Schedule model) 175 | { 176 | model.State = State.Canceled; 177 | _db.Update(model); 178 | 179 | if (model.UserId != null) 180 | { 181 | var msg = new MessageBuilder() 182 | .SetChatId(model.UserId.Value) 183 | .Push("Your booking was canceled"); //todo: replace text 184 | await _sender.Send(msg); 185 | } 186 | return model; 187 | } 188 | 189 | public ValueTask Free(int scheduleId) 190 | { 191 | var model = _repo.First(c => c.Id == scheduleId); 192 | model.State = State.Free; 193 | _db.Update(model); 194 | return new (model); 195 | } 196 | 197 | public ValueTask> GetFreeSlots(long userId, DateTime day, PageFilter page) 198 | { 199 | var date = day.Date; 200 | var tomorrow = date.AddDays(1); 201 | var query = _repo.Where(c => c.OwnerId == userId 202 | && c.From >= date 203 | && c.From < tomorrow 204 | && c.State == State.Free 205 | ).AsQueryable(); 206 | return new (_paging.Paging(query, page)); 207 | } 208 | 209 | public ValueTask> GetFreeDays(long userId, DateTime day, PageFilter page) 210 | { 211 | var query = _repo.Where(c => c.OwnerId == userId && c.From >= day && c.State == State.Free) 212 | .DistinctBy(c => c.From.Date) 213 | .Select(c => c.From.Date) 214 | .AsQueryable(); 215 | return new (_paging.Paging(query, page)); 216 | } 217 | 218 | public ValueTask> GetSchedulers(PageFilter filer) 219 | { 220 | var query = _users.Where(c => (c.Roles & UserRole.scheduler) == UserRole.scheduler).AsQueryable(); 221 | return new (_paging.Paging(query, filer)); 222 | } 223 | } 224 | 225 | 226 | public class ScheduleFilter : PageFilter 227 | { 228 | public State? State { get; set; } 229 | public int? OwnerId { get; set; } 230 | public int? UserId { get; set; } 231 | public DateTime? From { get; set; } 232 | public DateTime? To { get; set; } 233 | } 234 | 235 | public enum WeekDay 236 | { 237 | Sunday = 1, 238 | Monday = 1 << 1, 239 | Tuesday = 1 << 2, 240 | Wednesday = 1 << 3, 241 | Thursday = 1 << 4, 242 | Friday = 1 << 5, 243 | Saturday = 1 << 6, 244 | } 245 | 246 | public record CreateSeriesParams( 247 | long OwnerId, 248 | DateTime SeriesFrom, 249 | DateTime SeriesTo, 250 | WeekDay WeekDays, 251 | DateTime Start, 252 | DateTime To 253 | ); 254 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/UserService.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | using SQLite; 3 | 4 | public class UserService : IBotUserService 5 | { 6 | readonly TableQuery _users; 7 | static string[] _zeroRoles = new string[0]; 8 | static UserRole[] _roles = Enum.GetValues(); 9 | 10 | public UserService(TableQuery users) 11 | { 12 | _users = users; 13 | } 14 | 15 | public ValueTask<(string? id, string[]? roles)> GetUserIdWithRoles(long tgUserId) 16 | { 17 | var user = _users.FirstOrDefault(c => c.Id == tgUserId); 18 | if (user == null) 19 | { 20 | return new ((null, null)); 21 | } 22 | 23 | 24 | var id = user.Id.ToString(); 25 | var roles = GetRoles(user.Roles); 26 | return new ((id, roles)); 27 | } 28 | 29 | private string[] GetRoles(UserRole roles) 30 | { 31 | if(roles == UserRole.none) 32 | { 33 | return _zeroRoles; 34 | } 35 | 36 | return _roles.Where(c => ((int)c & (int)roles) == (int)c) 37 | .Select(c => c.ToString()) 38 | .ToArray(); 39 | } 40 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.ScheduleExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.SendAndUpdateMessagesExample/Deployf.Botf.SendAndUpdateMessagesExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.SendAndUpdateMessagesExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | 3 | // This example shows how to send messages and update it. 4 | class Program : BotfProgram 5 | { 6 | public static void Main(string[] args) => StartBot(args); 7 | 8 | [Action("/start", "start the bot")] 9 | public void Start() 10 | { 11 | PushLL($"Hello!"); 12 | PushL($"Auto send mesage feature: /autosend"); 13 | PushL($"Simple send: /simplesend"); 14 | PushL($"Send multiple messages in one action: /multisend"); 15 | PushL($"Updating the message: /update"); 16 | } 17 | 18 | [Action("/autosend")] 19 | public void AutoSend() 20 | { 21 | PushLL("All the text passed to Push* messages are buffered in controller's state and will be automatically send even without Send()"); 22 | Push("If you don't want to use this feature just disable it through configuration autosend (should be like this: YourToken?autosend=0)"); 23 | } 24 | 25 | [Action("/simplesend")] 26 | public async Task SimpleSend() 27 | { 28 | Push("Just a message sended though direct call to Send method"); 29 | await Send(); // be сareful, Send is asynchronous 30 | } 31 | 32 | [Action("/multisend")] 33 | public async Task MultiSend() 34 | { 35 | PushL("Hello!"); 36 | Push("Wait 1 second! I'm thinking..."); 37 | await Send(); 38 | 39 | // let's make delay between messages 40 | await Task.Delay(1000); 41 | 42 | Push("Thanks! I'm here!. But wait another second..."); 43 | await Send(); 44 | 45 | await Task.Delay(1000); 46 | 47 | Push("So this is my last message. Sorry, it's time to rest"); 48 | 49 | // for last message you don't need to call `Send()` because autosend feature does it for you! 50 | // await Send() 51 | } 52 | 53 | [Action("/update")] 54 | public async Task UpdateInfo() 55 | { 56 | PushL("Let's play a game: change this message!"); 57 | PushL("P.S. To change me use command /update with message id as first argument and new message text as second"); 58 | PushL("Should be like: /update 123 new_content"); // There is a limitation: string parameters must be without space symbols. We know about that and will fix it in future! 59 | var message = await Send(); // Send and Update methods returns a message object - you can get its ID 60 | 61 | await Send($"Message is is {message.MessageId}"); 62 | } 63 | 64 | [Action("/update")] 65 | public async Task UpdateMessage(int messageId, string newContent) 66 | { 67 | PushL("You change this message to:"); 68 | Push(newContent); // Update all the content 69 | 70 | MessageId = messageId; // here we tell to BotF that we want to update a certain message 71 | await Update(); 72 | } 73 | 74 | // in this method we catch all exceptions that causes in user's controllers 75 | // firstly - to show error if you send wrong messageId 76 | [On(Handle.Exception)] 77 | void ExceptionHandler(Exception e) 78 | { 79 | Push("Unhandled exception: "); 80 | Push(e.GetType().Name + ": "); 81 | Push(e.Message); 82 | } 83 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.SendAndUpdateMessagesExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.HelloExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5281", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.SendAndUpdateMessagesExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Deployf.Botf": "Trace" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.UnknownHandlingExample/Deployf.Botf.UnknownHandlingExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.UnknownHandlingExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | // This example shows how to handle unknown type of updates. 5 | // Unknown means all updates that is not handled by any of [Action("...")] attribute and other Handle's type. 6 | class Program : BotfProgram 7 | { 8 | public static void Main(string[] args) => StartBot(args); 9 | 10 | [Action("/start", "start the bot")] 11 | public void Start() 12 | { 13 | Push($"Send any kind of message to me, please!"); 14 | Button("Callback test", "data"); 15 | } 16 | 17 | // The handler processes only unhandled clicks to the buttons 18 | [On(Handle.Unknown)] 19 | [Filter(Filters.CallbackQuery)] 20 | public void UnknownCallback() 21 | { 22 | PushL("Unknown callback"); 23 | // StopHandling tells to the BotF that if there are other handlers, BotF must not call them after processing current handler. 24 | Context.StopHandling(); 25 | } 26 | 27 | // This handler processes only unknown commands 28 | // third argument tells to the BotF that this handler has `1` order in the queue of processing. 29 | // The higher the order -> the faster it will be processed 30 | // for example: 31 | // | order | handler | 32 | // +------------+---------------------+ 33 | // | 10 | Handler10 | 34 | // | 1 | Handler1 | 35 | // | 0(default) | DefaultHandler | 36 | // | -1 | HandlerUnderDefault | 37 | // 38 | // Handler10(with 10 order) will be processed first, then Handler1, then DefaultHandler(without specefic order) and HandlerUnderDefault will be processed last 39 | [On(Handle.Unknown, 1)] 40 | [Filter(Filters.Command)] 41 | public void UnknownCommand() 42 | { 43 | Reply(); 44 | PushL("Unknown command"); 45 | Context.StopHandling(); 46 | } 47 | 48 | // This handler has custom filter function. It means that you can implement your own filter function(see next method) 49 | // To provide the filter-function into the Botf you can tell the name of the function. 50 | // You can use just identifier of the function inside the same class as the handler, 51 | // or provide full path to the method, with namespace and declared class (like YourFancyNamespace.YourClass.FilterFunctionName) 52 | [On(Handle.Unknown, 1)] 53 | [Filter(nameof(Filter))] 54 | public void UnknownCustomFilter() 55 | { 56 | Reply(); 57 | PushL("Symbol is found"); 58 | Context.StopHandling(); 59 | } 60 | 61 | // Filter-function must be: 62 | // * static 63 | // * return bool value 64 | // * receive single argument with IUpdateContext type 65 | public static bool Filter(IUpdateContext ctx) 66 | { 67 | var message = ctx.Update.Message?.Text; 68 | if(string.IsNullOrEmpty(message)) 69 | { 70 | return false; 71 | } 72 | 73 | return message.Contains("filter"); 74 | } 75 | 76 | [On(Handle.Unknown)] 77 | [Filter(Filters.Contact)] 78 | public void UnknownContactHandler() 79 | { 80 | Reply(); 81 | PushL("Thank you, I will add you to my contact list"); 82 | Context.StopHandling(); 83 | } 84 | 85 | [On(Handle.Unknown)] 86 | [Filter(Filters.Location)] 87 | public void UnknownLocationHandler() 88 | { 89 | Reply(); 90 | PushL("Thank you, I will meet you there"); 91 | Context.StopHandling(); 92 | } 93 | 94 | // This handler will process all new text messages. 95 | // But if previus handler has called `Context.StopHandling()` it will not be handled. 96 | [On(Handle.Unknown)] 97 | [Filter(Filters.Text)] 98 | public void UnknownNewTextHandler() 99 | { 100 | Reply(); 101 | PushL("Unknown text message"); 102 | Context.StopHandling(); 103 | } 104 | 105 | // This handler catch all messages that contains text pattern like "Hello ***!" 106 | // And reply for it with "Hey!" 107 | [On(Handle.Unknown, 5)] 108 | [Filter(Filters.Text)] 109 | [Filter(And: Filters.Regex, Param: "Hello .*!")] 110 | public void UnknownTextHello() 111 | { 112 | Reply(); 113 | PushL("Hey!"); 114 | Context.StopHandling(); 115 | } 116 | 117 | // This handler will handle all other unhandled updates that was not handled yet. It's called as "general" handler 118 | [On(Handle.Unknown)] 119 | public void UnknownGeneralHandler() 120 | { 121 | Reply(); 122 | PushL("Geleral Handler"); 123 | } 124 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.UnknownHandlingExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.HelloExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5281", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.UnknownHandlingExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Deployf.Botf": "Trace" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.WebAppExample/Deployf.Botf.WebAppExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.WebAppExample/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | 4 | 5 |

Hello from webapp!

6 |

The time on the server is @DateTime.Now

7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.WebAppExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Deployf.Botf; 2 | 3 | BotfProgram.StartBot(args, 4 | onConfigure: (svc, conf) => { 5 | svc.AddRazorPages(); 6 | }, 7 | onRun: (app, conf) => { 8 | ((WebApplication)app).MapRazorPages(); 9 | } 10 | ); 11 | 12 | class WebAppExampleController : BotController 13 | { 14 | readonly IConfiguration conf; 15 | 16 | public WebAppExampleController(IConfiguration conf) 17 | { 18 | this.conf = conf; 19 | } 20 | 21 | [Action("/start", "start the bot")] 22 | public async Task Start() 23 | { 24 | PushL($"Hello!"); 25 | Push("This bot shows the example how to use webapps in BotF framework"); 26 | 27 | // we can show webapp button in three ways 28 | // first: call next line and pass just only text argument 29 | // `text` is a text on a button 30 | // webapp url will be used from global configuration(botf key in the appsettings.*.json) 31 | Button(WebApp("Inline webapp")); 32 | 33 | // second: pass the webapp through second parameter in WebApp method 34 | Button(WebApp("Google as webapp", conf["MyWebAppUrl"])); 35 | 36 | await Send(); 37 | 38 | Push("Or click on the keybord button below"); 39 | 40 | // and third - in the keyboard 41 | KButton(KWebApp("Open webapp")); 42 | } 43 | 44 | [On(Handle.Unknown)] 45 | void OnUnknown() 46 | { 47 | var webappData = Context.Update?.Message?.WebAppData; 48 | if(webappData == null) 49 | { 50 | return; 51 | } 52 | 53 | PushL("Data from webapp: " + webappData.Data); 54 | } 55 | } -------------------------------------------------------------------------------- /Examples/Deployf.Botf.WebAppExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Deployf.Botf.HelloExample": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:5281", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Deployf.Botf.WebAppExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | 10 | "MyWebAppUrl": "https://google.com" 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Deploy-f 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BotF 2 | [![Nuget](https://img.shields.io/nuget/v/Deployf.Botf)](https://www.nuget.org/packages/Deployf.Botf) [![GitHub](https://img.shields.io/github/license/deploy-f/botf)](https://github.com/deploy-f/botf/blob/master/LICENSE) [![CI](https://github.com/deploy-f/botf/actions/workflows/dotnet.yml/badge.svg)](https://github.com/deploy-f/botf/actions/workflows/dotnet.yml) [![Telegram Group](https://img.shields.io/endpoint?url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fbotf_community)](https://t.me/botf_community) 3 | 4 | Make beautiful and clear telegram bots with the asp.net-like architecture! 5 | 6 | BotF has next features: 7 | 8 | 🤘 long pooling and webhook mode without any changes in the code 9 | 😎 very convinient way to work with commands and reply buttons 10 | 👆 integrated pagination with buttons 11 | 🆔 authentication and role-based authorization 12 | 🔥 statemachine for complicated dialogs with users 13 | 🕸️ asp.net-like approach to develop bots 14 | ⚒️ automatic creating of command menu 15 | 🗓️ integrated DateTime picker 16 | 📤 auto sending 17 | 📲 webapp native support 18 | 🚤 good performance 19 | 20 | ## Documentaion 21 | 22 | 🔜 Documentation is under developement. We will push it here in the readme file soon. Feel free to ask us your questions in community chat in telegram [![Telegram Group](https://img.shields.io/endpoint?url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fbotf_community)](https://t.me/botf_community) 23 | 24 | ▶️ There is a good video on youtube https://www.youtube.com/watch?v=hieLnm9wO6s 25 | 26 | ## Install 27 | 28 | ```bash 29 | dotnet add package Deployf.Botf 30 | ``` 31 | 32 | ## Example 33 | 34 | Put next code into `Program.cs` file 35 | 36 | ```csharp 37 | using Deployf.Botf; 38 | 39 | class Program : BotfProgram 40 | { 41 | // It's boilerplate program entrypoint. 42 | // We just simplified all usual code into static method StartBot. 43 | // But in this case of starting of the bot, you should add a config section under "bot" key to appsettings.json 44 | public static void Main(string[] args) => StartBot(args); 45 | 46 | // Action attribute mean that you mark async method `Start` 47 | // as handler for user's text in message which equal to '/start' string. 48 | // You can name method as you want 49 | // And also, second argument of Action's attribute is a description for telegram's menu for this action 50 | [Action("/start", "start the bot")] 51 | public void Start() 52 | { 53 | // Just sending a reply message to user. Very simple, isn't? 54 | Push($"Send `{nameof(Hello)}` to me, please!"); 55 | } 56 | 57 | [Action(nameof(Hello))] 58 | public void Hello() 59 | { 60 | Push("Hey! Thank you! That's it."); 61 | } 62 | 63 | // Here we handle all unknown command or just text sent from user 64 | [On(Handle.Unknown)] 65 | public async Task Unknown() 66 | { 67 | // Here, we use the so-called "buffering of sending message" 68 | // It means you dont need to construct all message in the string and send it once 69 | // You can use Push to just add the text to result message, or PushL - the same but with new line after the string. 70 | PushL("You know.. it's very hard to recognize your command!"); 71 | PushL("Please, write a correct text. Or use /start command"); 72 | 73 | // And finally send buffered message 74 | await Send(); 75 | } 76 | } 77 | ``` 78 | 79 | And replace content of `appsettings.json` with your bot username and token: 80 | 81 | ``` 82 | { 83 | "botf": "123456778990:YourToken" 84 | } 85 | ``` 86 | 87 | And that's it! Veeery easy, isn't? 88 | Just run the program :) 89 | 90 | Other examples you can find in `/Examples` folder. 91 | 92 | ## Hosting 93 | 94 | After you develop your bot, you can deploy it to our hosting: [deploy-f.com](https://deploy-f.com) 95 | -------------------------------------------------------------------------------- /build/GetBuildVersion.psm1: -------------------------------------------------------------------------------- 1 | Function GetBuildVersion { 2 | Param ( 3 | [string]$VersionString 4 | ) 5 | 6 | # Process through regex 7 | $VersionString -match "(?\d+)(\.(?\d+))?(\.(?\d+))?(\-(?
[0-9A-Za-z\-\.]+))?(\+(?\d+))?" | Out-Null
 8 | 
 9 |     if ($matches -eq $null) {
10 |         return "1.0.0-build"
11 |     }
12 | 
13 |     # Extract the build metadata
14 |     $BuildRevision = [uint64]$matches['build']
15 |     # Extract the pre-release tag
16 |     $PreReleaseTag = [string]$matches['pre']
17 |     # Extract the patch
18 |     $Patch = [uint64]$matches['patch']
19 |     # Extract the minor
20 |     $Minor = [uint64]$matches['minor']
21 |     # Extract the major
22 |     $Major = [uint64]$matches['major']
23 | 
24 |     $Version = [string]$Major + '.' + [string]$Minor + '.' + [string]$Patch;
25 |     if ($PreReleaseTag -ne [string]::Empty) {
26 |         $Version = $Version + '-' + $PreReleaseTag
27 |     }
28 | 
29 |     if ($BuildRevision -ne 0) {
30 |         $Version = $Version + '.' + [string]$BuildRevision
31 |     }
32 | 
33 |     return $Version
34 | }


--------------------------------------------------------------------------------