Skip to content

Commit 9a3e791

Browse files
committed
controller state save and restore abillity
1 parent 5d852e2 commit 9a3e791

File tree

11 files changed

+274
-11
lines changed

11 files changed

+274
-11
lines changed

Deployf.Botf.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deployf.Botf.UnknownHandlin
3838
EndProject
3939
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deployf.Botf.ActionsAndQueryExample", "Examples\Deployf.Botf.ActionsAndQueryExample\Deployf.Botf.ActionsAndQueryExample.csproj", "{171EAF49-EE72-4499-A841-A7E4EDC7D4FF}"
4040
EndProject
41+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deployf.Botf.ControllerStateExample", "Examples\Deployf.Botf.ControllerStateExample\Deployf.Botf.ControllerStateExample.csproj", "{88DA6CD9-E658-4B6E-8657-734CA195F783}"
42+
EndProject
4143
Global
4244
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4345
Debug|Any CPU = Debug|Any CPU
@@ -96,6 +98,10 @@ Global
9698
{171EAF49-EE72-4499-A841-A7E4EDC7D4FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
9799
{171EAF49-EE72-4499-A841-A7E4EDC7D4FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
98100
{171EAF49-EE72-4499-A841-A7E4EDC7D4FF}.Release|Any CPU.Build.0 = Release|Any CPU
101+
{88DA6CD9-E658-4B6E-8657-734CA195F783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
102+
{88DA6CD9-E658-4B6E-8657-734CA195F783}.Debug|Any CPU.Build.0 = Debug|Any CPU
103+
{88DA6CD9-E658-4B6E-8657-734CA195F783}.Release|Any CPU.ActiveCfg = Release|Any CPU
104+
{88DA6CD9-E658-4B6E-8657-734CA195F783}.Release|Any CPU.Build.0 = Release|Any CPU
99105
EndGlobalSection
100106
GlobalSection(SolutionProperties) = preSolution
101107
HideSolutionNode = FALSE
@@ -113,6 +119,7 @@ Global
113119
{3C21F035-1A68-4782-B985-7BEB3600B501} = {90A07D9F-B417-4D28-BC58-5D987CB90430}
114120
{26897D3D-FDA0-4CA0-89AE-1B5EC777593B} = {90A07D9F-B417-4D28-BC58-5D987CB90430}
115121
{171EAF49-EE72-4499-A841-A7E4EDC7D4FF} = {90A07D9F-B417-4D28-BC58-5D987CB90430}
122+
{88DA6CD9-E658-4B6E-8657-734CA195F783} = {90A07D9F-B417-4D28-BC58-5D987CB90430}
116123
EndGlobalSection
117124
GlobalSection(ExtensibilityGlobals) = postSolution
118125
SolutionGuid = {558E8FF9-5AE8-4471-BF84-D79F5B0E91FB}

Deployf.Botf/Attributes/StateAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace Deployf.Botf;
22

3-
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
3+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
44
public sealed class StateAttribute : Attribute
55
{
66
public readonly string? Name;

Deployf.Botf/BotController.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ public abstract class BotController
1212
private const string LAST_MESSAGE_ID_KEY = "$last_message_id";
1313

1414
public UserClaims User { get; set; } = new UserClaims();
15-
protected long ChatId { get; private set; }
16-
protected long FromId { get; private set; }
17-
protected IUpdateContext Context { get; private set; } = null!;
15+
public long ChatId { get; private set; }
16+
public long FromId { get; private set; }
17+
public IUpdateContext Context { get; private set; } = null!;
1818
protected CancellationToken CancelToken { get; private set; }
1919
protected ITelegramBotClient Client { get; set; } = null!;
2020
protected MessageBuilder Message { get; set; } = new MessageBuilder();
21-
protected IKeyValueStorage? Store { get; set; }
21+
public IKeyValueStorage? Store { get; set; }
2222

2323
protected bool IsDirty
2424
{
@@ -167,13 +167,17 @@ public async Task Call<T>(Action<T> method) where T : BotController
167167
await controller.OnAfterCall();
168168
}
169169

170-
public virtual Task OnBeforeCall()
170+
public virtual async Task OnBeforeCall()
171171
{
172-
return Task.CompletedTask;
172+
var stateService = new BotControllerStateService();
173+
await stateService.Load(this);
173174
}
174175

175176
public virtual async Task OnAfterCall()
176177
{
178+
var stateService = new BotControllerStateService();
179+
await stateService.Save(this);
180+
177181
if (!(Context!.Bot is BotfBot bot))
178182
{
179183
return;

Deployf.Botf/Middlewares/BotControllersFSMMiddleware.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, Cance
4343
context.Items["args"] = new object[] { state };
4444
context.Items["action"] = value;
4545
context.Items["controller"] = controller;
46+
context.Items["skip_binding_marker"] = true;
4647

4748
afterNext = async () =>
4849
{
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using System.Reflection;
2+
3+
namespace Deployf.Botf;
4+
5+
public struct BotControllerStateService
6+
{
7+
public static Dictionary<Type, Func<BotController, Task>?> _savers = new ();
8+
public static Dictionary<Type, Func<BotController, Task>?> _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<FieldInfo> fields;
20+
List<PropertyInfo> 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<FieldInfo> fields;
62+
List<PropertyInfo> 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<FieldInfo> fields, out List<PropertyInfo> 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<StateAttribute>() != null)
117+
.ToList();
118+
props = controllerType
119+
.GetProperties(bindingFlags)
120+
.Where(c => c.GetCustomAttribute<StateAttribute>() != 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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public async ValueTask Invoke(IUpdateContext ctx, CancellationToken token, Metho
2020
{
2121
var controller = (BotController)_services.GetRequiredService(method.DeclaringType!);
2222
controller.Init(ctx, token);
23-
await InvokeInternal(controller, method, args, ctx);
23+
await InvokeInternal(controller, method, args, ctx, false);
2424
}
2525

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

3636
var method = (MethodInfo)context.Items["action"];
3737
var args = (object[])context.Items["args"];
38-
await InvokeInternal(controller, method, args, context);
38+
var skipBinding = context.Items.ContainsKey("skip_binding_marker");
39+
await InvokeInternal(controller, method, args, context, !skipBinding);
3940

4041
return true;
4142
}
4243

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

4748
_log.LogDebug("Begin execute action {Controller}.{Method}. Arguments: {@Args}",
4849
method.DeclaringType!.Name,

Examples/Deployf.Botf.ActionsAndQueryExample/Program.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ void ActionWithPrimitiveArgs(int arg1, string arg2)
4343
[Action]
4444
void ActionWithStoredValue(ExampleClass instance)
4545
{
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+
4655
PushL("Action with class as a parameter");
4756
PushL($"IntField: {instance.IntField}");
4857
PushL($"StringProp: {instance.StringProp}");
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<ProjectReference Include="..\..\Deployf.Botf\Deployf.Botf.csproj" />
11+
</ItemGroup>
12+
13+
</Project>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
}

0 commit comments

Comments
 (0)