diff --git a/Examples/DependencyInjection/Database/BotDbContext.cs b/Examples/DependencyInjection/Database/BotDbContext.cs new file mode 100644 index 0000000..b7e20f5 --- /dev/null +++ b/Examples/DependencyInjection/Database/BotDbContext.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; + +namespace DependencyInjection.Database; + +public class BotDbContext : DbContext +{ + public BotDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Users { get; set; } +} \ No newline at end of file diff --git a/Examples/DependencyInjection/Database/User.cs b/Examples/DependencyInjection/Database/User.cs new file mode 100644 index 0000000..3c01b63 --- /dev/null +++ b/Examples/DependencyInjection/Database/User.cs @@ -0,0 +1,7 @@ +namespace DependencyInjection.Database; + +public class User +{ + public long Id { get; set; } + public string LastMessage { get; set; } +} \ No newline at end of file diff --git a/Examples/DependencyInjection/DependencyInjection.csproj b/Examples/DependencyInjection/DependencyInjection.csproj new file mode 100644 index 0000000..eab1f5b --- /dev/null +++ b/Examples/DependencyInjection/DependencyInjection.csproj @@ -0,0 +1,19 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/Examples/DependencyInjection/Forms/ConfirmationForm.cs b/Examples/DependencyInjection/Forms/ConfirmationForm.cs new file mode 100644 index 0000000..9152242 --- /dev/null +++ b/Examples/DependencyInjection/Forms/ConfirmationForm.cs @@ -0,0 +1,67 @@ +using DependencyInjection.Database; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Base; +using TelegramBotBase.Form; +using TelegramBotBase.DependencyInjection; + +namespace DependencyInjection.Forms +{ + public class ConfirmationForm : FormBase + { + private readonly BotDbContext _dbContext; + + public ConfirmationForm(BotDbContext dbContext) + { + _dbContext = dbContext; + } + + public override async Task Load(MessageResult message) + { + var user = await _dbContext.Users.FindAsync(Device.DeviceId); + + if (user == null) + { + await this.NavigateTo(); + return; + } + } + + public override async Task Action(MessageResult message) + { + await message.ConfirmAction("Go back"); + + switch (message.RawData) + { + + case "back": + + await this.NavigateTo(); + + break; + + } + + + } + + public override async Task Render(MessageResult message) + { + var user = await _dbContext.Users.FindAsync(Device.DeviceId); + if (user == null) + return; + + var bf = new ButtonForm(); + bf.AddButtonRow("Back", "back"); + + await Device.Send($"ConfirmationForm: Your last message was: {user.LastMessage}. Click \"Back\" to get back.", bf); + } + + + + + } +} diff --git a/Examples/DependencyInjection/Program.cs b/Examples/DependencyInjection/Program.cs new file mode 100644 index 0000000..f33bf53 --- /dev/null +++ b/Examples/DependencyInjection/Program.cs @@ -0,0 +1,35 @@ +using DependencyInjection.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using TelegramBotBase.Builder; + +namespace DependencyInjection +{ + internal class Program + { + static async Task Main(string[] args) + { + + + var serviceCollection = new ServiceCollection() + .AddDbContext(x => x.UseInMemoryDatabase("TelegramBotBase")); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var bot = BotBaseBuilder.Create() + .WithAPIKey(Environment.GetEnvironmentVariable("API_KEY") ?? + throw new Exception("API_KEY is not set")) + .DefaultMessageLoop() + .WithServiceProvider(serviceProvider) + .NoProxy() + .NoCommands() + .NoSerialization() + .DefaultLanguage() + .Build(); + + await bot.Start(); + await Task.Delay(-1); + + } + } +} \ No newline at end of file diff --git a/Examples/DependencyInjection/StartForm.cs b/Examples/DependencyInjection/StartForm.cs new file mode 100644 index 0000000..92c6ec0 --- /dev/null +++ b/Examples/DependencyInjection/StartForm.cs @@ -0,0 +1,79 @@ +using DependencyInjection.Database; +using DependencyInjection.Forms; +using TelegramBotBase.Base; +using TelegramBotBase.Form; +using TelegramBotBase.DependencyInjection; + +namespace DependencyInjection; + +public class StartForm : FormBase +{ + private readonly BotDbContext _dbContext; + + public StartForm(BotDbContext dbContext) + { + _dbContext = dbContext; + } + + public override async Task Load(MessageResult message) + { + var user = await _dbContext.Users.FindAsync(Device.DeviceId); + if (user is null) + { + user = new User + { + Id = Device.DeviceId, + LastMessage = "" + }; + + _dbContext.Users.Add(user); + await _dbContext.SaveChangesAsync(); + } + + if (message.IsAction) + return; + + + user.LastMessage = string.IsNullOrWhiteSpace(message.MessageText) ? "" : message.MessageText; + await _dbContext.SaveChangesAsync(); + } + + public override async Task Action(MessageResult message) + { + await message.ConfirmAction("Ok"); + + switch(message.RawData) + { + + case "open": + + await this.NavigateTo(typeof(ConfirmationForm)); + + var new_form = await this.NavigateTo(); + + if (new_form == null) + { + await Device.Send("Cant open ConfirmationForm"); + } + + break; + + } + + + } + + public override async Task Render(MessageResult message) + { + var user = await _dbContext.Users.FindAsync(Device.DeviceId); + if (user == null) + return; + + var bf = new ButtonForm(); + + bf.AddButtonRow("Open confirmation", "open"); + + await Device.Send($"Your last message's text was: `{user.LastMessage}`", bf); + } + +} diff --git a/Examples/InlineAndReplyCombination/Program.cs b/Examples/InlineAndReplyCombination/Program.cs index 00372c7..fab456d 100644 --- a/Examples/InlineAndReplyCombination/Program.cs +++ b/Examples/InlineAndReplyCombination/Program.cs @@ -27,6 +27,8 @@ namespace InlineAndReplyCombination await BotBaseInstance.UploadBotCommands(); + BotBaseInstance.BotCommand += BotBaseInstance_BotCommand; + await BotBaseInstance.Start(); @@ -37,5 +39,28 @@ namespace InlineAndReplyCombination await BotBaseInstance.Stop(); } + + private static async Task BotBaseInstance_BotCommand(object sender, TelegramBotBase.Args.BotCommandEventArgs e) + { + + switch(e.Command) + { + case "/start": + + + var start = new StartForm(); + + await e.Device.ActiveForm.NavigateTo(start); + + + break; + + + + } + + + + } } } \ No newline at end of file diff --git a/README.md b/README.md index 778f4f7..4a20ed5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # .NET Telegram Bot Framework - Context based addon -[![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/v/TelegramBotBase.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase/) +[![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/vpre/TelegramBotBase.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase/) [![Telegram chat](https://img.shields.io/badge/Support_Chat-Telegram-blue.svg?style=flat-square)](https://www.t.me/tgbotbase) [![License](https://img.shields.io/github/license/MajMcCloud/telegrambotframework.svg?style=flat-square&maxAge=2592000&label=License)](https://raw.githubusercontent.com/MajMcCloud/TelegramBotFramework/master/LICENCE.md) @@ -1083,3 +1083,8 @@ Having already a web application and want to add a TelegramBot side-by-side with Want to use Inline- and ReplyMarkup at the same time ? Here is an example how you can do that: - [Examples/InlineAndReplyCombination](Examples/InlineAndReplyCombination) + + +Alpha: Full Dependency Injection example within this framework. + +- [Examples/DependencyInjection](Examples/DependencyInjection) \ No newline at end of file diff --git a/TelegramBotBase.Extensions.Serializer.Database.MSSQL/MSSQLSerializer.cs b/TelegramBotBase.Extensions.Serializer.Database.MSSQL/MSSQLSerializer.cs index ed08d95..4dd69bb 100644 --- a/TelegramBotBase.Extensions.Serializer.Database.MSSQL/MSSQLSerializer.cs +++ b/TelegramBotBase.Extensions.Serializer.Database.MSSQL/MSSQLSerializer.cs @@ -31,7 +31,7 @@ namespace TelegramBotBase.Extensions.Serializer.Database.MSSQL if (FallbackStateForm != null && !FallbackStateForm.IsSubclassOf(typeof(FormBase))) { - throw new ArgumentException("FallbackStateForm is not a subclass of FormBase"); + throw new ArgumentException($"{nameof(FallbackStateForm)} is not a subclass of {nameof(FormBase)}"); } } diff --git a/TelegramBotBase/Base/FormBase.cs b/TelegramBotBase/Base/FormBase.cs index 479e5ee..160952f 100644 --- a/TelegramBotBase/Base/FormBase.cs +++ b/TelegramBotBase/Base/FormBase.cs @@ -32,6 +32,8 @@ public class FormBase : IDisposable public MessageClient Client { get; set; } + IServiceProvider _serviceProvider = null; + /// /// has this formular already been disposed ? /// diff --git a/TelegramBotBase/Builder/BotBaseBuilder.cs b/TelegramBotBase/Builder/BotBaseBuilder.cs index ddbf4a8..497bed0 100644 --- a/TelegramBotBase/Builder/BotBaseBuilder.cs +++ b/TelegramBotBase/Builder/BotBaseBuilder.cs @@ -39,7 +39,10 @@ public class BotBaseBuilder : IAPIKeySelectionStage, IMessageLoopSelectionStage, /// private Dictionary> BotCommandScopes { get; } = new(); - + /// + /// Creates a full BotBase instance with all parameters previously set. + /// + /// public BotBase Build() { var bot = new BotBase(_apiKey, _client) diff --git a/TelegramBotBase/Builder/Interfaces/IBuildingStage.cs b/TelegramBotBase/Builder/Interfaces/IBuildingStage.cs index 17e49cc..1e92cc4 100644 --- a/TelegramBotBase/Builder/Interfaces/IBuildingStage.cs +++ b/TelegramBotBase/Builder/Interfaces/IBuildingStage.cs @@ -2,5 +2,9 @@ public interface IBuildingStage { + /// + /// Creates a full BotBase instance with all parameters previously set. + /// + /// BotBase Build(); } \ No newline at end of file diff --git a/TelegramBotBase/DependencyInjection/Extensions.cs b/TelegramBotBase/DependencyInjection/Extensions.cs new file mode 100644 index 0000000..f9a0bfc --- /dev/null +++ b/TelegramBotBase/DependencyInjection/Extensions.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Form; + +namespace TelegramBotBase.DependencyInjection +{ + public static class Extensions + { + internal static FieldInfo _ServiceProviderField = typeof(FormBase).GetField("_serviceProvider", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + + /// + /// Use Dependency Injection to create new form and inject parameters. (Main variant) + /// + /// + /// + /// + public static async Task NavigateTo(this FormBase current_form, params object[] args) + where NewForm : FormBase + { + var _serviceProvider = current_form.GetServiceProvider(); + + var instance = ActivatorUtilities.CreateInstance(_serviceProvider, typeof(NewForm)) as NewForm; + + if (instance == null) + return null; //throw new Exception("Could not instantiate new form via DI."); + + instance.SetServiceProvider(_serviceProvider); + + await current_form.NavigateTo(instance, args); + + return instance; + } + + /// + /// Use Dependency Injection to create new form and inject parameters. (Alternative variant) + /// + /// + /// + /// + /// + /// + public static async Task NavigateTo(this FormBase current_form, Type formBaseType, params object[] args) + { + if (!typeof(FormBase).IsAssignableFrom(formBaseType)) + throw new ArgumentException($"{nameof(formBaseType)} argument must be a {nameof(FormBase)} type"); + + var _serviceProvider = current_form.GetServiceProvider(); + + var instance = ActivatorUtilities.CreateInstance(_serviceProvider, formBaseType) as FormBase; + + if (instance == null) + return null; //throw new Exception("Could not instantiate new form via DI."); + + instance.SetServiceProvider(_serviceProvider); + + await current_form.NavigateTo(instance, args); + + return instance; + } + + /// + /// Sets the internal service provider field. + /// + /// + /// + public static void SetServiceProvider(this FormBase form, IServiceProvider serviceProvider) + { + _ServiceProviderField?.SetValue(form, serviceProvider); + } + + /// + /// Gets the internal service provider field value. + /// + /// + /// + public static IServiceProvider GetServiceProvider(this FormBase form) + { + var sp = _ServiceProviderField?.GetValue(form) as IServiceProvider; + return sp; + } + } +} diff --git a/TelegramBotBase/Factories/DefaultStartFormFactory.cs b/TelegramBotBase/Factories/DefaultStartFormFactory.cs index 7ce2988..a908c85 100644 --- a/TelegramBotBase/Factories/DefaultStartFormFactory.cs +++ b/TelegramBotBase/Factories/DefaultStartFormFactory.cs @@ -12,7 +12,7 @@ public class DefaultStartFormFactory : IStartFormFactory { if (!typeof(FormBase).IsAssignableFrom(startFormClass)) { - throw new ArgumentException("startFormClass argument must be a FormBase type"); + throw new ArgumentException($"{nameof(startFormClass)} argument must be a {nameof(FormBase)} type"); } _startFormClass = startFormClass; diff --git a/TelegramBotBase/Factories/ServiceProviderStartFormFactory.cs b/TelegramBotBase/Factories/ServiceProviderStartFormFactory.cs index 4ee81db..8bac651 100644 --- a/TelegramBotBase/Factories/ServiceProviderStartFormFactory.cs +++ b/TelegramBotBase/Factories/ServiceProviderStartFormFactory.cs @@ -1,5 +1,6 @@ using System; using Microsoft.Extensions.DependencyInjection; +using TelegramBotBase.DependencyInjection; using TelegramBotBase.Form; using TelegramBotBase.Interfaces; @@ -14,16 +15,21 @@ public class ServiceProviderStartFormFactory : IStartFormFactory { if (!typeof(FormBase).IsAssignableFrom(startFormClass)) { - throw new ArgumentException("startFormClass argument must be a FormBase type"); + throw new ArgumentException($"{nameof(startFormClass)} argument must be a {nameof(FormBase)} type"); } - + _startFormClass = startFormClass; _serviceProvider = serviceProvider; } public FormBase CreateForm() { - return (FormBase)ActivatorUtilities.CreateInstance(_serviceProvider, _startFormClass); + var fb = (FormBase)ActivatorUtilities.CreateInstance(_serviceProvider, _startFormClass); + + //Sets an internal field for future ServiceProvider navigation + fb.SetServiceProvider(_serviceProvider); + + return fb; } } diff --git a/TelegramBotBase/MessageLoops/FormBaseMessageLoop.cs b/TelegramBotBase/MessageLoops/FormBaseMessageLoop.cs index 0069c1b..4bec2a0 100644 --- a/TelegramBotBase/MessageLoops/FormBaseMessageLoop.cs +++ b/TelegramBotBase/MessageLoops/FormBaseMessageLoop.cs @@ -72,6 +72,12 @@ public class FormBaseMessageLoop : IMessageLoopFactory } } + //Message edited ? + if(update.Type == UpdateType.EditedMessage) + { + await activeForm.Edited(mr); + } + //Action Event if (!session.FormSwitched && mr.IsAction) { diff --git a/TelegramBotBase/MessageLoops/FullMessageLoop.cs b/TelegramBotBase/MessageLoops/FullMessageLoop.cs index 2c4b1b9..1affd19 100644 --- a/TelegramBotBase/MessageLoops/FullMessageLoop.cs +++ b/TelegramBotBase/MessageLoops/FullMessageLoop.cs @@ -65,6 +65,12 @@ public class FullMessageLoop : IMessageLoopFactory } } + //Message edited ? + if (update.Type == UpdateType.EditedMessage) + { + await activeForm.Edited(mr); + } + //Action Event if (!session.FormSwitched && mr.IsAction) { diff --git a/TelegramBotBase/States/JSONStateMachine.cs b/TelegramBotBase/States/JSONStateMachine.cs index ada7fe4..5c764a6 100644 --- a/TelegramBotBase/States/JSONStateMachine.cs +++ b/TelegramBotBase/States/JSONStateMachine.cs @@ -29,7 +29,7 @@ public class JsonStateMachine : IStateMachine if (FallbackStateForm != null && !FallbackStateForm.IsSubclassOf(typeof(FormBase))) { - throw new ArgumentException("FallbackStateForm is not a subclass of FormBase"); + throw new ArgumentException($"{nameof(FallbackStateForm)} is not a subclass of {nameof(FormBase)}"); } FilePath = file ?? throw new ArgumentNullException(nameof(file)); diff --git a/TelegramBotBase/States/SimpleJSONStateMachine.cs b/TelegramBotBase/States/SimpleJSONStateMachine.cs index 7e8fa58..e722305 100644 --- a/TelegramBotBase/States/SimpleJSONStateMachine.cs +++ b/TelegramBotBase/States/SimpleJSONStateMachine.cs @@ -30,7 +30,7 @@ public class SimpleJsonStateMachine : IStateMachine if (FallbackStateForm != null && !FallbackStateForm.IsSubclassOf(typeof(FormBase))) { - throw new ArgumentException("FallbackStateForm is not a subclass of FormBase"); + throw new ArgumentException($"{nameof(FallbackStateForm)} is not a subclass of {nameof(FormBase)}"); } FilePath = file ?? throw new ArgumentNullException(nameof(file)); diff --git a/TelegramBotBase/States/XMLStateMachine.cs b/TelegramBotBase/States/XMLStateMachine.cs index 1a569eb..fdaea1a 100644 --- a/TelegramBotBase/States/XMLStateMachine.cs +++ b/TelegramBotBase/States/XMLStateMachine.cs @@ -27,7 +27,7 @@ public class XmlStateMachine : IStateMachine if (FallbackStateForm != null && !FallbackStateForm.IsSubclassOf(typeof(FormBase))) { - throw new ArgumentException("FallbackStateForm is not a subclass of FormBase"); + throw new ArgumentException($"{nameof(FallbackStateForm)} is not a subclass of {nameof(FormBase)}"); } FilePath = file ?? throw new ArgumentNullException(nameof(file)); diff --git a/TelegramBotBase/TelegramBotBase.csproj b/TelegramBotBase/TelegramBotBase.csproj index 2e16171..0967e33 100644 --- a/TelegramBotBase/TelegramBotBase.csproj +++ b/TelegramBotBase/TelegramBotBase.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netcoreapp3.1;net6 + netstandard2.0;netcoreapp3.1;net6;net7 10 false False diff --git a/TelegramBotFramework.sln b/TelegramBotFramework.sln index 885b8a0..8fd0e98 100644 --- a/TelegramBotFramework.sln +++ b/TelegramBotFramework.sln @@ -30,7 +30,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCoreBot", "Examples\EFCor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotAndWebApplication", "Examples\BotAndWebApplication\BotAndWebApplication.csproj", "{52EA3201-02E8-46F5-87C4-B4752C8A815C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InlineAndReplyCombination", "Examples\InlineAndReplyCombination\InlineAndReplyCombination.csproj", "{067E8EBE-F90A-4AFF-A0FF-20578216486E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InlineAndReplyCombination", "Examples\InlineAndReplyCombination\InlineAndReplyCombination.csproj", "{067E8EBE-F90A-4AFF-A0FF-20578216486E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyInjection", "Examples\DependencyInjection\DependencyInjection.csproj", "{689B16BC-200E-4C68-BB2E-8B209070849B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -78,6 +80,10 @@ Global {067E8EBE-F90A-4AFF-A0FF-20578216486E}.Debug|Any CPU.Build.0 = Debug|Any CPU {067E8EBE-F90A-4AFF-A0FF-20578216486E}.Release|Any CPU.ActiveCfg = Release|Any CPU {067E8EBE-F90A-4AFF-A0FF-20578216486E}.Release|Any CPU.Build.0 = Release|Any CPU + {689B16BC-200E-4C68-BB2E-8B209070849B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {689B16BC-200E-4C68-BB2E-8B209070849B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {689B16BC-200E-4C68-BB2E-8B209070849B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {689B16BC-200E-4C68-BB2E-8B209070849B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -91,6 +97,7 @@ Global {261BED47-0404-4A9A-86FC-047DE42A7D25} = {BFA71E3F-31C0-4FC1-A320-4DCF704768C5} {52EA3201-02E8-46F5-87C4-B4752C8A815C} = {BFA71E3F-31C0-4FC1-A320-4DCF704768C5} {067E8EBE-F90A-4AFF-A0FF-20578216486E} = {BFA71E3F-31C0-4FC1-A320-4DCF704768C5} + {689B16BC-200E-4C68-BB2E-8B209070849B} = {BFA71E3F-31C0-4FC1-A320-4DCF704768C5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {59CB40E1-9FA7-4867-A56F-4F418286F057}