Skip to content

Commit

Permalink
controller state save and restore abillity
Browse files Browse the repository at this point in the history
  • Loading branch information
k0dep committed Aug 6, 2022
1 parent 5d852e2 commit 9a3e791
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 11 deletions.
7 changes: 7 additions & 0 deletions Deployf.Botf.sln
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deployf.Botf.UnknownHandlin
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deployf.Botf.ActionsAndQueryExample", "Examples\Deployf.Botf.ActionsAndQueryExample\Deployf.Botf.ActionsAndQueryExample.csproj", "{171EAF49-EE72-4499-A841-A7E4EDC7D4FF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deployf.Botf.ControllerStateExample", "Examples\Deployf.Botf.ControllerStateExample\Deployf.Botf.ControllerStateExample.csproj", "{88DA6CD9-E658-4B6E-8657-734CA195F783}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -96,6 +98,10 @@ Global
{171EAF49-EE72-4499-A841-A7E4EDC7D4FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{171EAF49-EE72-4499-A841-A7E4EDC7D4FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{171EAF49-EE72-4499-A841-A7E4EDC7D4FF}.Release|Any CPU.Build.0 = Release|Any CPU
{88DA6CD9-E658-4B6E-8657-734CA195F783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88DA6CD9-E658-4B6E-8657-734CA195F783}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88DA6CD9-E658-4B6E-8657-734CA195F783}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88DA6CD9-E658-4B6E-8657-734CA195F783}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -113,6 +119,7 @@ Global
{3C21F035-1A68-4782-B985-7BEB3600B501} = {90A07D9F-B417-4D28-BC58-5D987CB90430}
{26897D3D-FDA0-4CA0-89AE-1B5EC777593B} = {90A07D9F-B417-4D28-BC58-5D987CB90430}
{171EAF49-EE72-4499-A841-A7E4EDC7D4FF} = {90A07D9F-B417-4D28-BC58-5D987CB90430}
{88DA6CD9-E658-4B6E-8657-734CA195F783} = {90A07D9F-B417-4D28-BC58-5D987CB90430}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {558E8FF9-5AE8-4471-BF84-D79F5B0E91FB}
Expand Down
2 changes: 1 addition & 1 deletion Deployf.Botf/Attributes/StateAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Deployf.Botf;

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class StateAttribute : Attribute
{
public readonly string? Name;
Expand Down
16 changes: 10 additions & 6 deletions Deployf.Botf/BotController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ public abstract class BotController
private const string LAST_MESSAGE_ID_KEY = "$last_message_id";

public UserClaims User { get; set; } = new UserClaims();
protected long ChatId { get; private set; }
protected long FromId { get; private set; }
protected IUpdateContext Context { get; private set; } = null!;
public long ChatId { get; private set; }
public long FromId { get; private set; }
public IUpdateContext Context { get; private set; } = null!;
protected CancellationToken CancelToken { get; private set; }
protected ITelegramBotClient Client { get; set; } = null!;
protected MessageBuilder Message { get; set; } = new MessageBuilder();
protected IKeyValueStorage? Store { get; set; }
public IKeyValueStorage? Store { get; set; }

protected bool IsDirty
{
Expand Down Expand Up @@ -167,13 +167,17 @@ public async Task Call<T>(Action<T> method) where T : BotController
await controller.OnAfterCall();
}

public virtual Task OnBeforeCall()
public virtual async Task OnBeforeCall()
{
return Task.CompletedTask;
var stateService = new BotControllerStateService();
await stateService.Load(this);
}

public virtual async Task OnAfterCall()
{
var stateService = new BotControllerStateService();
await stateService.Save(this);

if (!(Context!.Bot is BotfBot bot))
{
return;
Expand Down
1 change: 1 addition & 0 deletions Deployf.Botf/Middlewares/BotControllersFSMMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, Cance
context.Items["args"] = new object[] { state };
context.Items["action"] = value;
context.Items["controller"] = controller;
context.Items["skip_binding_marker"] = true;

afterNext = async () =>
{
Expand Down
128 changes: 128 additions & 0 deletions Deployf.Botf/System/BotControllerStateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Reflection;

namespace Deployf.Botf;

public struct BotControllerStateService
{
public static Dictionary<Type, Func<BotController, Task>?> _savers = new ();
public static Dictionary<Type, Func<BotController, Task>?> _loaders = new ();

public async Task Load(BotController controller)
{
var controllerType = controller.GetType();
if (_loaders.TryGetValue(controllerType, out var loader) && loader != null)
{
await loader(controller);
return;
}

List<FieldInfo> fields;
List<PropertyInfo> props;
ExtractMembers(controllerType, out fields, out props);

if (fields.Count > 0 || props.Count > 0)
{
loader = async (BotController _controller) =>
{
var storage = _controller.Store!;
foreach (var field in fields)
{
var value = await storage.Get(_controller.FromId, GetKey(field, controllerType), null);
field.SetValue(_controller, value);
}
foreach (var prop in props)
{
var value = await storage.Get(_controller.FromId, GetKey(prop, controllerType), null);
prop.SetValue(_controller, value);
}
};

_loaders[controllerType] = loader;

await loader(controller);
}
else
{
_loaders[controllerType] = null;
}
}

public async Task Save(BotController controller)
{
var controllerType = controller.GetType();
if (_savers.TryGetValue(controllerType, out var saver) && saver != null)
{
await saver(controller);
return;
}

List<FieldInfo> fields;
List<PropertyInfo> props;
ExtractMembers(controllerType, out fields, out props);

if (fields.Count > 0 || props.Count > 0)
{
saver = async (BotController _controller) =>
{
var storage = _controller.Store!;
foreach (var field in fields)
{
var value = field.GetValue(_controller);
var key = GetKey(field, controllerType);
if(value != null)
{
await storage.Set(_controller.FromId, key, value);
}
else
{
await storage.Remove(_controller.FromId, key);
}
}
foreach (var prop in props)
{
var value = prop.GetValue(_controller);
var key = GetKey(prop, controllerType);
if(value != null)
{
await storage.Set(_controller.FromId, key, value);
}
else
{
await storage.Remove(_controller.FromId, key);
}
}
};

_savers[controllerType] = saver;

await saver(controller);
}
else
{
_savers[controllerType] = null;
}
}

static void ExtractMembers(Type controllerType, out List<FieldInfo> fields, out List<PropertyInfo> props)
{
const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;

fields = controllerType
.GetFields(bindingFlags)
.Where(c => c.GetCustomAttribute<StateAttribute>() != null)
.ToList();
props = controllerType
.GetProperties(bindingFlags)
.Where(c => c.GetCustomAttribute<StateAttribute>() != null)
.ToList();
}

static string GetKey(MemberInfo member, Type controllerType)
{
return $"$ctrl-state_{controllerType.Name}.{member.Name}";
}
}
9 changes: 5 additions & 4 deletions Deployf.Botf/System/BotControllersInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public async ValueTask Invoke(IUpdateContext ctx, CancellationToken token, Metho
{
var controller = (BotController)_services.GetRequiredService(method.DeclaringType!);
controller.Init(ctx, token);
await InvokeInternal(controller, method, args, ctx);
await InvokeInternal(controller, method, args, ctx, false);
}

public async ValueTask<bool> Invoke(IUpdateContext context)
Expand All @@ -35,14 +35,15 @@ public async ValueTask<bool> Invoke(IUpdateContext context)

var method = (MethodInfo)context.Items["action"];
var args = (object[])context.Items["args"];
await InvokeInternal(controller, method, args, context);
var skipBinding = context.Items.ContainsKey("skip_binding_marker");
await InvokeInternal(controller, method, args, context, !skipBinding);

return true;
}

private async ValueTask<object?> InvokeInternal(BotController controller, MethodInfo method, object[] args, IUpdateContext ctx)
private async ValueTask<object?> InvokeInternal(BotController controller, MethodInfo method, object[] args, IUpdateContext ctx, bool bind = true)
{
var typedParams = await _binder.Bind(method, args, ctx);
var typedParams = bind ? await _binder.Bind(method, args, ctx) : args;

_log.LogDebug("Begin execute action {Controller}.{Method}. Arguments: {@Args}",
method.DeclaringType!.Name,
Expand Down
9 changes: 9 additions & 0 deletions Examples/Deployf.Botf.ActionsAndQueryExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ void ActionWithPrimitiveArgs(int arg1, string arg2)
[Action]
void ActionWithStoredValue(ExampleClass instance)
{
if(instance == null)
{
instance = new ExampleClass()
{
IntField = -1,
StringProp = "The data was lost :( probably you had rebooted the application"
};
}

PushL("Action with class as a parameter");
PushL($"IntField: {instance.IntField}");
PushL($"StringProp: {instance.StringProp}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Deployf.Botf\Deployf.Botf.csproj" />
</ItemGroup>

</Project>
78 changes: 78 additions & 0 deletions Examples/Deployf.Botf.ControllerStateExample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Deployf.Botf;

BotfProgram.StartBot(args);

class ControllerStateExample : BotController
{
[State]
ExampleClass _data;

[State]
int intField;

int nonStateIntField;

[Action("/start", "start the bot")]
public void Start()
{
PushL($"Hello!");
PushL("This is an example of how to store controllers state(fields and props) through updates");
PushL("Current controller state:");
DumpState();

PushL();
PushL("For refresh call /start");

RowButton("Set random _data", Q(SetRandom_data));
RowButton("Set random intField", Q(SetRandom_intField));
RowButton("Set random nonStateIntField", Q(SetRandom_nonStateIntField));
}

[Action]
void SetRandom_data()
{
_data = new ExampleClass()
{
IntField = Random.Shared.Next(),
StringProp = Guid.NewGuid().ToString()
};
Start();
}

[Action]
void SetRandom_intField()
{
intField = Random.Shared.Next();
Start();
}

[Action]
void SetRandom_nonStateIntField()
{
nonStateIntField = Random.Shared.Next();
Start();
}

void DumpState()
{
if(_data == null)
{
PushL("_data is null");
}
else
{
PushL($"_data is {_data.ToString()}");
}

PushL($"intField is {intField}");
PushL($"nonStateIntField is {nonStateIntField}");
}
}

class ExampleClass
{
public int IntField;
public string StringProp { get; set; }

public override string ToString() => $"({IntField}, {StringProp})";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"profiles": {
"Deployf.Botf.HelloExample": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5281",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
9 changes: 9 additions & 0 deletions Examples/Deployf.Botf.ControllerStateExample/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

0 comments on commit 9a3e791

Please sign in to comment.