using com.sun.jmx.mbeanserver; using Comal.Classes; using Expressive; using InABox.Core; using InABox.Database; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace PRS.Shared.Events; public interface IEventData { IEvent Event { get; } IEnumerable ReferencedVariables { get; } } public class EventData : IEventData where T : IEvent where TDataModel : IEventDataModel { public T Event { get; set; } /// /// A list of triggers for this event. If any of the triggers match, the event runs. /// public List> Triggers { get; set; } public List> Actions { get; set; } IEvent IEventData.Event => Event; public IEnumerable ReferencedVariables => Triggers.SelectMany(x => x.ReferencedVariables) .Concat(Actions.SelectMany(x => x.ReferencedVariables)) .Distinct(); public EventData(T eventData) { Event = eventData; Triggers = new List>(); Actions = new List>(); } public Notification GenerateNotification(TDataModel model) => Event.GenerateNotification(model); } public static class EventUtils { #region Serialisation private class WritablePropertiesOnlyResolver : DefaultContractResolver { protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { IList props = base.CreateProperties(type, memberSerialization); return props.Where(p => p.Writable).ToList(); } } private static JsonSerializerSettings SerializationSettings() { var settings = Serialization.CreateSerializerSettings(); settings.TypeNameHandling = TypeNameHandling.Auto; settings.ContractResolver = new WritablePropertiesOnlyResolver(); return settings; } public static string Serialize(IEventData data) { return JsonConvert.SerializeObject(data, typeof(IEventData), SerializationSettings()); } public static IEventData Deserialize(string json) { return JsonConvert.DeserializeObject(json, SerializationSettings())!; } #endregion #region Event Types private static Dictionary>? _eventTriggerTypes; private static Dictionary>? _eventActionTypes; private static List? _standardEventActionTypes; [MemberNotNullWhen(true, nameof(_eventTriggerTypes), nameof(_eventActionTypes), nameof(_standardEventActionTypes))] private static bool _loadedTypes { get; set; } [MemberNotNull(nameof(_eventTriggerTypes), nameof(_eventActionTypes), nameof(_standardEventActionTypes))] private static void LoadTypes() { if (_loadedTypes) return; _loadedTypes = true; _eventTriggerTypes = new(); _eventActionTypes = new(); _standardEventActionTypes = new(); foreach(var type in CoreUtils.TypeList(x => true)) { if (type.GetInterfaceDefinition(typeof(IEventTrigger<,>)) is Type eventTriggerInterface) { if (type.GetConstructor([]) is not null) { var eventType = eventTriggerInterface.GenericTypeArguments[0]; eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType; _eventTriggerTypes.GetValueOrAdd(eventType).Add(type); } } else if(type.GetInterfaceDefinition(typeof(IStandardEventAction<>)) is Type standardEventAction) { if (type.GetConstructor([]) is not null) { _standardEventActionTypes.Add(type); } } else if (type.GetInterfaceDefinition(typeof(IEventAction<>)) is Type eventActionInterface) { if (type.GetConstructor([]) is not null) { var eventType = eventActionInterface.GenericTypeArguments[0]; eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType; _eventActionTypes.GetValueOrAdd(eventType).Add(type); } } } } public static IEnumerable GetEventTriggerTypes(Type eventType) { LoadTypes(); eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType; return _eventTriggerTypes.GetValueOrDefault(eventType) ?? Enumerable.Empty(); } public static IEnumerable GetEventActionTypes(Type eventType) { LoadTypes(); eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType; return _eventActionTypes.GetValueOrDefault(eventType) ?? Enumerable.Empty(); } public static IEnumerable GetStandardEventActionTypes() { LoadTypes(); return _standardEventActionTypes; } #endregion #region Execution private static bool Check(EventData ev, TDataModel dataModel) where T : IEvent where TDataModel : IEventDataModel { return ev.Triggers.Count == 0 || ev.Triggers.Any(x => x.Check(dataModel)); } public static void Run(IStore store, Event ev, EventData evData, TDataModel dataModel) where T : IEvent where TDataModel : IEventDataModel { if (!Check(evData, dataModel)) return; foreach(var action in evData.Actions) { IEvent.RunAction(evData.Event, dataModel, action); } NotifySubscribers(store, ev, evData, dataModel); } public static void SendNotifications(IStore store, IEnumerable<(EventSubscriberType type, Notification notification)> notifications) { var toSave = new List(); foreach(var (type, notification) in notifications) { if(type == EventSubscriberType.Notification) { toSave.Add(notification); } else if(type == EventSubscriberType.Email) { var mailer = DbFactory.Mailer; if(mailer is not null) { if (mailer.Connect()) { var msg = mailer.CreateMessage(); // msg.From = DbFactory.EmailAddress; msg.To = [notification.Employee.Email]; msg.Subject = notification.Title; msg.Body = notification.Description; var bOK = mailer.SendMessage(msg); } } } } store.Provider.Save(toSave); } private static void NotifySubscribers(IStore store, Event ev, EventData evData, TDataModel dataModel) where T : IEvent where TDataModel : IEventDataModel { if (!ev.NotificationsEnabled) return; string? description; if (ev.NotificationExpression.IsNullOrWhiteSpace()) { description = null; } else { var descriptionExpr = new CoreExpression(ev.NotificationExpression, typeof(string)); if(!descriptionExpr.TryEvaluate(dataModel).Get(out description, out var error)) { CoreUtils.LogException(store.UserID, error, extra: "Error notifying subscribers", store.Logger); return; } } var subscribers = store.Provider.Query( new Filter(x => x.Event.ID).IsEqualTo(ev.ID), Columns.None().Add(x => x.Employee.ID).Add(x => x.Employee.Email).Add(x => x.SubscriberType)) .ToArray(); SendNotifications(store, subscribers.Select(x => { var notification = evData.GenerateNotification(dataModel); notification.Employee.CopyFrom(x.Employee); if (description is not null) { notification.Description = description; } return (x.SubscriberType, notification); })); } #endregion #region Event Cache private static bool _loadedCache = false; private static Dictionary>> _entityEvents = new(); private static Dictionary _entityEventMap = new(); private static Dictionary> _genericEvents = new(); private static Dictionary _genericEventMap = new(); public static Type GetEventType(EventType type) { switch (type) { case EventType.AfterSave: return typeof(SaveEvent<>); case EventType.Scheduled: return typeof(ScheduledEvent); default: throw new Exception($"Invalid event type {type}"); } } public static void ReloadCache(IProvider provider) { _loadedCache = true; _entityEvents.Clear(); _entityEventMap.Clear(); _genericEvents.Clear(); _genericEventMap.Clear(); var events = provider.Query( new Filter(x => x.Enabled).IsEqualTo(true), Columns.None().Add(x => x.ID).Add(x => x.Code).Add(x => x.EventType).Add(x => x.Data).Add(x => x.NotificationExpression).Add(x => x.NotificationsEnabled)) .ToObjects(); foreach(var ev in events) { AddEvent(provider, ev); } } private static void AddGenericEvent(Event ev) { if(ev.Data is not null && ev.Data.Length != 0) { var data = Deserialize(ev.Data); var list = _genericEvents.GetValueOrAdd(ev.EventType); list.RemoveAll(x => x.Event.ID == ev.ID); list.Add((ev, data)); _genericEventMap[ev.ID] = ev.EventType; } } private static void RemoveGenericEvent(Event ev) { if(_genericEventMap.Remove(ev.ID, out var evType)) { if(_genericEvents.TryGetValue(evType, out var eventTypeEvents)) { eventTypeEvents.RemoveAll(x => x.Event.ID == ev.ID); if(eventTypeEvents.Count == 0) { _genericEvents.Remove(evType); } } } } private static void AddEntityEvent(Event ev) { if(ev.Data is not null && ev.Data.Length != 0) { var data = Deserialize(ev.Data); var entityType = data.Event.GetType().GenericTypeArguments[0]; var list = _entityEvents.GetValueOrAdd(ev.EventType).GetValueOrAdd(entityType); list.RemoveAll(x => x.Event.ID == ev.ID); list.Add((ev, data)); _entityEventMap[ev.ID] = (ev.EventType, entityType); } } private static void RemoveEntityEvent(Event ev) { if(_entityEventMap.TryGetValue(ev.ID, out var item)) { if(_entityEvents.TryGetValue(item.Item1, out var eventTypeEvents)) { if(eventTypeEvents.TryGetValue(item.Item2, out var entityEvents)) { entityEvents.RemoveAll(x => x.Event.ID == ev.ID); if(entityEvents.Count == 0) { eventTypeEvents.Remove(item.Item2); } } if(eventTypeEvents.Count == 0) { _entityEvents.Remove(item.Item1); } } _entityEventMap.Remove(ev.ID); } } public static void AddEvent(IProvider provider, Event ev) { if (!_loadedCache) ReloadCache(provider); var type = GetEventType(ev.EventType); if (type.HasInterface(typeof(IEntityEvent<>))) { AddEntityEvent(ev); } else { AddGenericEvent(ev); } } public static void RemoveEvent(IProvider provider, Event ev) { if (!_loadedCache) ReloadCache(provider); var type = GetEventType(ev.EventType); if (type.HasInterface(typeof(IEntityEvent<>))) { RemoveEntityEvent(ev); } else { RemoveGenericEvent(ev); } } #endregion #region Public Event Running Interface public static void AfterSave(IStore store, T entity) where T : Entity, new() { if (!_loadedCache) ReloadCache(store.Provider); var events = _entityEvents.GetValueOrDefault(EventType.AfterSave) ?.GetValueOrDefault(typeof(T)); if (events is null) return; foreach(var ev in events) { var eventData = (ev.EventData as EventData, SaveEventDataModel>)!; var model = new SaveEventDataModel(entity, store); eventData.Event.Init(store, ev.Event, eventData, model); Run(store, ev.Event, eventData, model); } } private static bool CheckEventSchedule(ScheduledEvent ev) { var now = DateTime.Now; switch (ev.Properties.Period) { case SchedulePeriod.Minute: case SchedulePeriod.Hour: case SchedulePeriod.Day: var dayOfWeekSettings = ev.Properties.DayOfWeekSettings.FirstOrDefault(x => x.DayOfWeek == now.DayOfWeek); if(dayOfWeekSettings is null || !dayOfWeekSettings.Enabled) { return false; } var start = now - now.TimeOfDay + dayOfWeekSettings.StartTime; var end = now - now.TimeOfDay + dayOfWeekSettings.EndTime; if(ev.Properties.Period == SchedulePeriod.Day) { if(ev.Properties.LastExecution < start && now >= start && (now.Date - ev.Properties.LastExecution.Date).TotalDays >= ev.Properties.Frequency) { ev.Properties.LastExecution = start; return true; } else { return false; } } else if(ev.Properties.Period == SchedulePeriod.Hour) { if(start <= now && now <= end) { var nowIntervals = Math.Floor((now - start).TotalHours / ev.Properties.Frequency); var lastIntervals = Math.Floor((ev.Properties.LastExecution - start).TotalHours / ev.Properties.Frequency); if(nowIntervals > lastIntervals) { ev.Properties.LastExecution = start.AddHours(nowIntervals * ev.Properties.Frequency); } return true; } else { return false; } } else if(ev.Properties.Period == SchedulePeriod.Minute) { if (start <= now && now <= end) { var nowIntervals = Math.Floor((now - start).TotalMinutes / ev.Properties.Frequency); var lastIntervals = Math.Floor((ev.Properties.LastExecution - start).TotalMinutes / ev.Properties.Frequency); if (nowIntervals > lastIntervals) { ev.Properties.LastExecution = start.AddMinutes(nowIntervals * ev.Properties.Frequency); } return true; } else { return false; } } else { throw new Exception("Invalid state"); } case SchedulePeriod.Week: if (now >= ev.Properties.NextSchedule) { while (now >= ev.Properties.NextSchedule) { ev.Properties.NextSchedule += TimeSpan.FromDays(7 * ev.Properties.Frequency); } return true; } else { return false; } case SchedulePeriod.Month: if(now >= ev.Properties.NextSchedule) { while(now >= ev.Properties.NextSchedule) { ev.Properties.NextSchedule = ev.Properties.NextSchedule.AddMonths(ev.Properties.Frequency); } return true; } else { return false; } case SchedulePeriod.Year: if(now >= ev.Properties.NextSchedule) { while(now >= ev.Properties.NextSchedule) { ev.Properties.NextSchedule = ev.Properties.NextSchedule.AddYears(ev.Properties.Frequency); } return true; } else { return false; } default: throw new Exception("Invalid state"); } } public static void CheckScheduledEvents() { var store = DbFactory.FindStore(Guid.Empty, "", Platform.DatabaseEngine, CoreUtils.GetVersion(), Logger.New()); if (!_loadedCache) ReloadCache(store.Provider); var events = _genericEvents.GetValueOrDefault(EventType.Scheduled); if (events is null) return; foreach(var ev in events) { var eventData = (ev.EventData as EventData)!; if (CheckEventSchedule(eventData.Event)) { var model = new ScheduledEventDataModel(ev.Event); Run(store, ev.Event, eventData, model); ev.Event.Data = Serialize(eventData); store.Provider.Save(ev.Event); } } } #endregion } #region DataModel Definition public interface IEventVariable { string Name { get; set; } Type Type { get; set; } } public class StandardEventVariable : IEventVariable { public string Name { get; set; } public Type Type { get; set; } public StandardEventVariable(string name, Type type) { Name = name; Type = type; } } public class ListEventVariable : IEventVariable { public string Name { get; set; } public Type Type { get; set; } public ListEventVariable(string name, Type type) { Name = name; Type = type; } } public interface IEventDataModelDefinition { IEnumerable GetVariables(); IEventVariable? GetVariable(string name); } #endregion #region DataModel public interface IEventDataModel : IVariableProvider { bool TryGetVariable(string name, out object? value); bool IVariableProvider.TryGetValue(string variableName, out object? value) { return TryGetVariable(variableName, out value); } T RootModel() where T : IEventDataModel { if(this is IChildEventDataModel child) { return child.Parent.RootModel(); } else if(this is T root) { return root; } else { throw new Exception($"Root model of wrong type; expected {typeof(T).FullName}; got {GetType().FullName}"); } } } public interface ITypedEventDataModel : IEventDataModel { public Type EntityType { get; } } public interface IChildEventDataModel : IEventDataModel { IEventDataModel Parent { get; } } public class ChildEventDataModel : IChildEventDataModel { public IEventDataModel Parent { get; set; } public Dictionary Values { get; set; } = new Dictionary(); public ChildEventDataModel(IEventDataModel parent) { Parent = parent; } public bool TryGetVariable(string name, out object? value) { return Values.TryGetValue(name, out value) || Parent.TryGetVariable(name, out value); } public void Init(IEventData data) { } } #endregion public interface IEvent { IEventDataModelDefinition DataModelDefinition(); public static void RunAction(IEvent ev, IEventDataModel model, IEventAction action) { var dataModelDef = ev.DataModelDefinition(); var values = new List<(string name, object? value)[]>() { Array.Empty<(string, object?)>() }; foreach(var variable in action.ReferencedVariables) { var varDef = dataModelDef.GetVariable(variable); if(varDef is ListEventVariable) { if (model.TryGetVariable(varDef.Name, out var list) && list is IEnumerable enumerable) { var oldValues = values; values = new List<(string name, object? value)[]>(); foreach(var item in enumerable) { foreach(var valueList in oldValues) { values.Add(valueList.Concatenate(new (string name, object? value)[] { (varDef.Name, item) })); } } } else { values.Clear(); break; } } } if(values.Count > 0 && (values.Count > 1 || values[0].Length > 0)) { var subModel = new ChildEventDataModel(model); foreach(var valueSet in values) { subModel.Values.Clear(); foreach(var (name, value) in valueSet) { subModel.Values[name] = value; } action.Execute(subModel); } } else { action.Execute(model); } } } /// /// Indicates that this event type has properties that can be edited; is used to edit these properties /// with a dynamic editor. /// public interface IPropertiesEvent where TProperties : BaseObject, new() { /// /// Create the properties object for editing. /// TProperties GetProperties(); /// /// Given an edited properties object, save them again. /// void SetProperties(TProperties properties); } /// /// Indicates that this event is generic for type ; if this interface is implemented, /// the main type must be generic, with only one generic type argument. /// public interface IEntityEvent where T : Entity { } public interface IEvent : IEvent { Notification GenerateNotification(TDataModel model); } public interface IEventTrigger { IEnumerable ReferencedVariables { get; } string Description { get; } } public interface IEventTrigger : IEventTrigger where TEvent : IEvent where TDataModel : IEventDataModel { bool Check(TDataModel dataModel); } public interface IEventTriggerParent : IEventTrigger where TEvent : IEvent where TDataModel : IEventDataModel { public List> ChildTriggers { get; } } public class AndTrigger : IEventTrigger, IEventTriggerParent where TEvent : IEvent where TDataModel : IEventDataModel { public List> ChildTriggers { get; set; } = new(); public IEnumerable ReferencedVariables => ChildTriggers.SelectMany(x => x.ReferencedVariables); public string Description => "And"; public bool Check(TDataModel dataModel) { return ChildTriggers.All(x => x.Check(dataModel)); } } public class OrTrigger : IEventTrigger, IEventTriggerParent where TEvent : IEvent where TDataModel : IEventDataModel { public List> ChildTriggers { get; set; } = new(); public IEnumerable ReferencedVariables => ChildTriggers.SelectMany(x => x.ReferencedVariables); public string Description => "Or"; public bool Check(TDataModel dataModel) { return ChildTriggers.Any(x => x.Check(dataModel)); } } public interface IEventAction { IEnumerable ReferencedVariables { get; } object? Execute(IEventDataModel dataModel); string Description { get; } } public interface IEventAction : IEventAction where TEvent : IEvent { } /// /// Marks this event action as being applicable to any event type. /// public interface IStandardEventAction : IEventAction where TEvent : IEvent { }