Adding StateMachine for Session serialization
- adding multiple classes and interfaces for Session Serialization and recovery after restart
This commit is contained in:
parent
8b9929198a
commit
0cdb8c3a1a
58
TelegramBotBase/Args/LoadStateEventArgs.cs
Normal file
58
TelegramBotBase/Args/LoadStateEventArgs.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace TelegramBotBase.Args
|
||||
{
|
||||
public class LoadStateEventArgs
|
||||
{
|
||||
public Dictionary<String,object> Values { get; set; }
|
||||
|
||||
public LoadStateEventArgs()
|
||||
{
|
||||
Values = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
public List<String> Keys
|
||||
{
|
||||
get
|
||||
{
|
||||
return Values.Keys.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public String Get(String key)
|
||||
{
|
||||
return Values[key].ToString();
|
||||
}
|
||||
|
||||
public int GetInt(String key)
|
||||
{
|
||||
int i = 0;
|
||||
if (int.TryParse(Values[key].ToString(), out i))
|
||||
return i;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public double GetDouble(String key)
|
||||
{
|
||||
double d = 0;
|
||||
if (double.TryParse(Values[key].ToString(), out d))
|
||||
return d;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool GetBool(String key)
|
||||
{
|
||||
bool b = false;
|
||||
if (bool.TryParse(Values[key].ToString(), out b))
|
||||
return b;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
37
TelegramBotBase/Args/SaveStateEventArgs.cs
Normal file
37
TelegramBotBase/Args/SaveStateEventArgs.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace TelegramBotBase.Args
|
||||
{
|
||||
public class SaveStateEventArgs
|
||||
{
|
||||
public Dictionary<String, object> Values { get; set; }
|
||||
|
||||
public SaveStateEventArgs()
|
||||
{
|
||||
Values = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
public void Set(String key, String value)
|
||||
{
|
||||
Values[key] = value;
|
||||
}
|
||||
|
||||
public void SetInt(String key, int value)
|
||||
{
|
||||
Values[key] = value;
|
||||
}
|
||||
|
||||
public void SetBool(String key, bool value)
|
||||
{
|
||||
Values[key] = value;
|
||||
}
|
||||
|
||||
public void SetDouble(String key, double value)
|
||||
{
|
||||
Values[key] = value;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
20
TelegramBotBase/Args/SaveStatesEventArgs.cs
Normal file
20
TelegramBotBase/Args/SaveStatesEventArgs.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using TelegramBotBase.Base;
|
||||
using TelegramBotBase.Sessions;
|
||||
|
||||
namespace TelegramBotBase.Args
|
||||
{
|
||||
public class SaveStatesEventArgs
|
||||
{
|
||||
public StateContainer States { get; set; }
|
||||
|
||||
|
||||
public SaveStatesEventArgs(StateContainer states)
|
||||
{
|
||||
this.States = states;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
TelegramBotBase/Attributes/SaveState.cs
Normal file
15
TelegramBotBase/Attributes/SaveState.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace TelegramBotBase.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Declares that the field or property should be save and recovered an restart.
|
||||
/// </summary>
|
||||
public class SaveState : Attribute
|
||||
{
|
||||
public String Key { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
34
TelegramBotBase/Base/StateContainer.cs
Normal file
34
TelegramBotBase/Base/StateContainer.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace TelegramBotBase.Base
|
||||
{
|
||||
public partial class StateContainer
|
||||
{
|
||||
public List<StateEntry> States { get; set; }
|
||||
|
||||
public List<long> ChatIds
|
||||
{
|
||||
get
|
||||
{
|
||||
return States.Where(a => a.DeviceId > 0).Select(a => a.DeviceId).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public List<long> GroupIds
|
||||
{
|
||||
get
|
||||
{
|
||||
return States.Where(a => a.DeviceId < 0).Select(a => a.DeviceId).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public StateContainer()
|
||||
{
|
||||
this.States = new List<StateEntry>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
44
TelegramBotBase/Base/StateEntry.cs
Normal file
44
TelegramBotBase/Base/StateEntry.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace TelegramBotBase.Base
|
||||
{
|
||||
|
||||
[DebuggerDisplay("Device: {DeviceId}, {FormUri}")]
|
||||
public class StateEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the DeviceId of the entry.
|
||||
/// </summary>
|
||||
public long DeviceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains the Username (on privat chats) or Group title on groups/channels.
|
||||
/// </summary>
|
||||
public String ChatTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains additional values to save.
|
||||
/// </summary>
|
||||
public Dictionary<String, object> Values { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains the full qualified namespace of the form to used for reload it via reflection.
|
||||
/// </summary>
|
||||
public String FormUri {get;set;}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the assembly, where to find that form.
|
||||
/// </summary>
|
||||
public String QualifiedName { get; set; }
|
||||
|
||||
public StateEntry()
|
||||
{
|
||||
this.Values = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -63,6 +63,10 @@ namespace TelegramBotBase
|
||||
/// </summary>
|
||||
public bool LogAllMessages { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Enable the SessionState (you need to implement on call forms the IStateForm interface)
|
||||
/// </summary>
|
||||
public IStateMachine StateMachine { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often could a form navigate to another (within one user action/call/message)
|
||||
@ -169,6 +173,10 @@ namespace TelegramBotBase
|
||||
this.Client.MessageEdit += Client_MessageEdit;
|
||||
this.Client.Action += Client_Action;
|
||||
|
||||
if (this.StateMachine != null)
|
||||
{
|
||||
LoadSessionStates();
|
||||
}
|
||||
|
||||
this.Client.TelegramClient.StartReceiving();
|
||||
}
|
||||
@ -186,6 +194,11 @@ namespace TelegramBotBase
|
||||
this.Client.Action -= Client_Action;
|
||||
|
||||
this.Client.TelegramClient.StopReceiving();
|
||||
|
||||
if (this.StateMachine != null)
|
||||
{
|
||||
SaveSessionStates();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -461,6 +474,145 @@ namespace TelegramBotBase
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the previously saved states from the machine.
|
||||
/// </summary>
|
||||
private async void LoadSessionStates()
|
||||
{
|
||||
if (this.StateMachine == null)
|
||||
{
|
||||
throw new ArgumentNullException("StateMachine", "No StateMachine defined. Please set one to property BotBase.StateMachine");
|
||||
}
|
||||
|
||||
var container = this.StateMachine.LoadFormStates();
|
||||
|
||||
foreach (var s in container.States)
|
||||
{
|
||||
Type t = Type.GetType(s.QualifiedName);
|
||||
if (t == null || !t.IsSubclassOf(typeof(FormBase)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var form = t.GetConstructor(new Type[] { }).Invoke(new object[] { }) as FormBase;
|
||||
|
||||
if (s.Values != null && s.Values.Count > 0)
|
||||
{
|
||||
var properties = s.Values.Where(a => a.Key.StartsWith("$"));
|
||||
var fields = form.GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic).Where(a => a.GetCustomAttributes(typeof(SaveState), true).Length != 0).ToList();
|
||||
|
||||
foreach (var p in properties)
|
||||
{
|
||||
var f = fields.FirstOrDefault(a => a.Name == p.Key.Substring(1));
|
||||
if (f == null)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
f.SetValue(form, p.Value);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Is Subclass of IStateForm
|
||||
var iform = form as IStateForm;
|
||||
if (iform != null)
|
||||
{
|
||||
var ls = new LoadStateEventArgs();
|
||||
ls.Values = s.Values;
|
||||
iform.LoadState(ls);
|
||||
}
|
||||
|
||||
|
||||
form.Client = Client;
|
||||
var device = new DeviceSession(s.DeviceId, form);
|
||||
|
||||
device.ChatTitle = s.ChatTitle;
|
||||
|
||||
this.Sessions.SessionList.Add(s.DeviceId, device);
|
||||
|
||||
await form.OnInit(new InitEventArgs());
|
||||
|
||||
await form.OnOpened(new EventArgs());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves all open states into the machine.
|
||||
/// </summary>
|
||||
private void SaveSessionStates()
|
||||
{
|
||||
if (this.StateMachine == null)
|
||||
{
|
||||
throw new ArgumentNullException("StateMachine", "No StateMachine defined. Please set one to property BotBase.StateMachine");
|
||||
}
|
||||
|
||||
var states = new List<StateEntry>();
|
||||
|
||||
foreach (var s in Sessions.SessionList)
|
||||
{
|
||||
if (s.Value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var form = s.Value.ActiveForm;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var se = new StateEntry();
|
||||
se.DeviceId = s.Key;
|
||||
se.ChatTitle = s.Value.ChatTitle;
|
||||
se.FormUri = form.GetType().FullName;
|
||||
se.QualifiedName = form.GetType().AssemblyQualifiedName;
|
||||
|
||||
//Is Subclass of IStateForm
|
||||
var iform = form as IStateForm;
|
||||
if (iform != null)
|
||||
{
|
||||
//Loading Session states
|
||||
SaveStateEventArgs ssea = new SaveStateEventArgs();
|
||||
iform.SaveState(ssea);
|
||||
|
||||
se.Values = ssea.Values;
|
||||
}
|
||||
|
||||
//Search for public properties with SaveState attribute
|
||||
var fields = form.GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic).Where(a => a.GetCustomAttributes(typeof(SaveState), true).Length != 0).ToList();
|
||||
|
||||
foreach (var f in fields)
|
||||
{
|
||||
var val = f.GetValue(form);
|
||||
|
||||
se.Values.Add("$" + f.Name, val);
|
||||
}
|
||||
|
||||
states.Add(se);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//Continue on error (skip this form)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var sc = new StateContainer();
|
||||
sc.States = states;
|
||||
|
||||
this.StateMachine.SaveFormStates(new SaveStatesEventArgs(sc));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Will be called if a session/context gets started
|
||||
/// </summary>
|
||||
|
||||
20
TelegramBotBase/Interfaces/IStateForm.cs
Normal file
20
TelegramBotBase/Interfaces/IStateForm.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using TelegramBotBase.Args;
|
||||
using TelegramBotBase.Base;
|
||||
|
||||
namespace TelegramBotBase.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Is used to save specific fields into a session state to survive restarts or unhandled exceptions and crashes.
|
||||
/// </summary>
|
||||
public interface IStateForm
|
||||
{
|
||||
|
||||
void LoadState(LoadStateEventArgs e);
|
||||
|
||||
void SaveState(SaveStateEventArgs e);
|
||||
|
||||
}
|
||||
}
|
||||
15
TelegramBotBase/Interfaces/IStateMachine.cs
Normal file
15
TelegramBotBase/Interfaces/IStateMachine.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using TelegramBotBase.Args;
|
||||
using TelegramBotBase.Base;
|
||||
|
||||
namespace TelegramBotBase.Interfaces
|
||||
{
|
||||
public interface IStateMachine
|
||||
{
|
||||
void SaveFormStates(SaveStatesEventArgs e);
|
||||
|
||||
StateContainer LoadFormStates();
|
||||
}
|
||||
}
|
||||
83
TelegramBotBase/States/JSONStateMachine.cs
Normal file
83
TelegramBotBase/States/JSONStateMachine.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization.Formatters;
|
||||
using System.Text;
|
||||
using TelegramBotBase.Args;
|
||||
using TelegramBotBase.Base;
|
||||
using TelegramBotBase.Interfaces;
|
||||
|
||||
namespace TelegramBotBase.States
|
||||
{
|
||||
/// <summary>
|
||||
/// Is used for all complex data types. Use if other default machines are not working.
|
||||
/// </summary>
|
||||
public class JSONStateMachine : IStateMachine
|
||||
{
|
||||
public String FilePath { get; set; }
|
||||
|
||||
public bool Overwrite { get; set; }
|
||||
|
||||
public JSONStateMachine(String file, bool overwrite = true)
|
||||
{
|
||||
if (file is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
this.FilePath = file;
|
||||
this.Overwrite = overwrite;
|
||||
}
|
||||
|
||||
public StateContainer LoadFormStates()
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = System.IO.File.ReadAllText(FilePath);
|
||||
|
||||
var sc = Newtonsoft.Json.JsonConvert.DeserializeObject<StateContainer>(content, new JsonSerializerSettings
|
||||
{
|
||||
TypeNameHandling = TypeNameHandling.All,
|
||||
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
|
||||
}) as StateContainer;
|
||||
|
||||
return sc;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return new StateContainer();
|
||||
}
|
||||
|
||||
public void SaveFormStates(SaveStatesEventArgs e)
|
||||
{
|
||||
if (System.IO.File.Exists(FilePath))
|
||||
{
|
||||
if (!this.Overwrite)
|
||||
{
|
||||
throw new Exception("File exists already.");
|
||||
}
|
||||
|
||||
System.IO.File.Delete(FilePath);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var content = Newtonsoft.Json.JsonConvert.SerializeObject(e.States, Formatting.Indented, new JsonSerializerSettings
|
||||
{
|
||||
TypeNameHandling = TypeNameHandling.All,
|
||||
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
|
||||
});
|
||||
|
||||
System.IO.File.WriteAllText(FilePath, content);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
75
TelegramBotBase/States/SimpleJSONStateMachine.cs
Normal file
75
TelegramBotBase/States/SimpleJSONStateMachine.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization.Formatters;
|
||||
using System.Text;
|
||||
using TelegramBotBase.Args;
|
||||
using TelegramBotBase.Base;
|
||||
using TelegramBotBase.Interfaces;
|
||||
|
||||
namespace TelegramBotBase.States
|
||||
{
|
||||
/// <summary>
|
||||
/// Is used for simple object structures like classes, lists or basic datatypes without generics and other compiler based data types.
|
||||
/// </summary>
|
||||
public class SimpleJSONStateMachine : IStateMachine
|
||||
{
|
||||
public String FilePath { get; set; }
|
||||
|
||||
public bool Overwrite { get; set; }
|
||||
|
||||
public SimpleJSONStateMachine(String file, bool overwrite = true)
|
||||
{
|
||||
if (file is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
this.FilePath = file;
|
||||
this.Overwrite = overwrite;
|
||||
}
|
||||
|
||||
public StateContainer LoadFormStates()
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = System.IO.File.ReadAllText(FilePath);
|
||||
|
||||
var sc = Newtonsoft.Json.JsonConvert.DeserializeObject<StateContainer>(content) as StateContainer;
|
||||
|
||||
return sc;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return new StateContainer();
|
||||
}
|
||||
|
||||
public void SaveFormStates(SaveStatesEventArgs e)
|
||||
{
|
||||
if (System.IO.File.Exists(FilePath))
|
||||
{
|
||||
if (!this.Overwrite)
|
||||
{
|
||||
throw new Exception("File exists already.");
|
||||
}
|
||||
|
||||
System.IO.File.Delete(FilePath);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var content = Newtonsoft.Json.JsonConvert.SerializeObject(e.States, Formatting.Indented);
|
||||
|
||||
System.IO.File.WriteAllText(FilePath, content);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
88
TelegramBotBase/States/XMLStateMachine.cs
Normal file
88
TelegramBotBase/States/XMLStateMachine.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
using TelegramBotBase.Args;
|
||||
using TelegramBotBase.Base;
|
||||
using TelegramBotBase.Interfaces;
|
||||
|
||||
namespace TelegramBotBase.States
|
||||
{
|
||||
public class XMLStateMachine : IStateMachine
|
||||
{
|
||||
public String FilePath { get; set; }
|
||||
|
||||
public bool Overwrite { get; set; }
|
||||
|
||||
public XMLStateMachine(String file, bool overwrite = true)
|
||||
{
|
||||
if (file is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
this.FilePath = file;
|
||||
this.Overwrite = overwrite;
|
||||
}
|
||||
|
||||
public StateContainer LoadFormStates()
|
||||
{
|
||||
try
|
||||
{
|
||||
DataContractSerializer serializer = new DataContractSerializer(typeof(StateContainer));
|
||||
|
||||
using (var reader = new StreamReader(FilePath))
|
||||
{
|
||||
using (var xml = new XmlTextReader(reader))
|
||||
{
|
||||
StateContainer sc = serializer.ReadObject(xml) as StateContainer;
|
||||
return sc;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return new StateContainer();
|
||||
}
|
||||
|
||||
public void SaveFormStates(SaveStatesEventArgs e)
|
||||
{
|
||||
if (System.IO.File.Exists(FilePath))
|
||||
{
|
||||
if (!this.Overwrite)
|
||||
{
|
||||
throw new Exception("File exists already.");
|
||||
}
|
||||
|
||||
System.IO.File.Delete(FilePath);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DataContractSerializer serializer = new DataContractSerializer(typeof(StateContainer));
|
||||
|
||||
using (var sw = new StreamWriter(this.FilePath))
|
||||
{
|
||||
using (var writer = new XmlTextWriter(sw))
|
||||
{
|
||||
writer.Formatting = Formatting.Indented; // indent the Xml so it’s human readable
|
||||
serializer.WriteObject(writer, e.States);
|
||||
writer.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user