Browse Source

Added notifications system to pipe ipc client.

Kenric Nugteren 2 years ago
parent
commit
b91c1e768b

+ 5 - 4
InABox.Client.Local/LocalClient.cs

@@ -32,7 +32,7 @@ namespace InABox.Clients
         protected override void NotifySession<TNotification>(Guid session, TNotification notification) =>
             NotifySession(session, typeof(TNotification), notification);
 
-        protected override void NotifySession(Guid session, Type TNotification, object? notification)
+        protected override void NotifySession(Guid session, Type TNotification, BaseObject notification)
         {
             if (session == ClientFactory.SessionID)
             {
@@ -45,8 +45,9 @@ namespace InABox.Clients
     {
         public LocalClient()
         {
-            Notify.Notifier = new LocalNotifier();
-            Notify.Notifier.Poll(ClientFactory.SessionID);
+            var notifier = new LocalNotifier();
+            Notify.AddNotifier(notifier);
+            Notify.Poll(ClientFactory.SessionID);
         }
 
         public override IEnumerable<string> SupportedTypes()
@@ -115,7 +116,7 @@ namespace InABox.Clients
 
         #region Load
 
-        protected override TEntity[] DoLoad(Filter<TEntity> filter = null, SortOrder<TEntity> sort = null)
+        protected override TEntity[] DoLoad(Filter<TEntity>? filter = null, SortOrder<TEntity>? sort = null)
         {
             var store = DbFactory.FindStore<TEntity>(ClientFactory.UserGuid, ClientFactory.UserID, ClientFactory.Platform, ClientFactory.Version);
             var result = store.Load(filter, sort);

+ 9 - 9
InABox.Core/Client/ClientFactory.cs

@@ -24,11 +24,11 @@ namespace InABox.Clients
 
     public static class ClientFactory
     {
-        
-        
+
+
         public static Dictionary<EmailType, Type> MailerTypes = new Dictionary<EmailType, Type>();
-        
-        
+
+
         public static Guid UserGuid { get; private set; }
 
         public static string UserID { get; private set; }
@@ -36,9 +36,9 @@ namespace InABox.Clients
         public static Guid UserSecurityID { get; private set; }
 
         public static Guid SessionID { get; set; }
-        
 
-        public static string? Platform { get; private set; }
+
+        public static Platform Platform { get; private set; }
 
         public static string? Version { get; private set; }
         
@@ -126,10 +126,10 @@ namespace InABox.Clients
             //return result;
         }
 
-        public static void SetClientType(Type type, string? platform, string? version, params object[]? parameters)
+        public static void SetClientType(Type type, Platform platform, string? version, params object[]? parameters)
         {
             ClientType = type;
-            Platform = String.IsNullOrEmpty(platform) ? Platform : platform;
+            Platform = platform;
             Version = String.IsNullOrEmpty(version) ? Version : version;
             Parameters = parameters == null ? Parameters : parameters;
             SupportedTypes = parameters == null ? SupportedTypes : null;
@@ -138,7 +138,7 @@ namespace InABox.Clients
         public static void ClearClientType()
         {
             ClientType = null;
-            Platform = "";
+            Platform = default(Platform);
             Version = "";
             Parameters = null;
         }

+ 8 - 3
InABox.Core/Client/Request.cs

@@ -13,7 +13,7 @@ namespace InABox.Clients
         [Obsolete]
         public string? Password { get; set; }
 
-        public string Platform { get; set; }
+        public Platform Platform { get; set; }
         public string Version { get; set; }
         public Guid Session { get; set; }
 
@@ -21,7 +21,7 @@ namespace InABox.Clients
         {
             writer.Write(UserID ?? "");
             writer.Write(Password ?? "");
-            writer.Write(Platform);
+            writer.Write(Platform.ToString());
             writer.Write(Version);
             writer.Write(Session);
         }
@@ -30,7 +30,12 @@ namespace InABox.Clients
         {
             UserID = reader.ReadString();
             Password = reader.ReadString();
-            Platform = reader.ReadString();
+
+            if(Enum.TryParse<Platform>(reader.ReadString(), out var platform))
+            {
+                Platform = platform;
+            }
+
             Version = reader.ReadString();
             Session = reader.ReadGuid();
         }

+ 9 - 27
InABox.Core/Notifications/Notifier.cs

@@ -14,9 +14,10 @@ namespace InABox.Core
         /// Polls for notifications, called on client connection.
         /// </summary>
         /// <returns>New notifications</returns>
-        IEnumerable<object> Poll(Guid session);
+        IEnumerable<BaseObject> Poll(Guid session);
     }
     public class PollHandler<TNotification> : IPollHandler
+        where TNotification : BaseObject
     {
         public delegate IEnumerable<TNotification> PollEvent(Guid session);
 
@@ -31,17 +32,15 @@ namespace InABox.Core
             OnPoll += poll;
         }
 
-        public IEnumerable<object> Poll(Guid session) => (OnPoll?.Invoke(session) ?? Array.Empty<TNotification>()).Cast<object>();
+        public IEnumerable<BaseObject> Poll(Guid session) => OnPoll?.Invoke(session) ?? Array.Empty<TNotification>();
     }
 
 
     public abstract class Notifier
     {
-        private List<IPollHandler> Handlers = new List<IPollHandler>();
-
         protected abstract void NotifyAll<TNotification>(TNotification notification) where TNotification : BaseObject;
         protected abstract void NotifySession<TNotification>(Guid session, TNotification notification) where TNotification : BaseObject;
-        protected abstract void NotifySession(Guid session, Type TNotification, object? notification);
+        protected abstract void NotifySession(Guid session, Type TNotification, BaseObject notification);
         protected abstract IEnumerable<Guid> GetUserSessions(Guid user);
         protected abstract IEnumerable<Guid> GetSessions(Platform platform);
 
@@ -51,6 +50,11 @@ namespace InABox.Core
             NotifyAll(notification);
         }
 
+        public void Push(Guid session, Type TNotification, BaseObject notification)
+        {
+            NotifySession(session, TNotification, notification);
+        }
+
         public void Push<TNotification>(Guid session, TNotification notification)
             where TNotification : BaseObject
         {
@@ -73,27 +77,5 @@ namespace InABox.Core
             }
         }
 
-        public void Poll(Guid session)
-        {
-            foreach (var handler in Handlers)
-            {
-                foreach (var notification in handler.Poll(session))
-                {
-                    NotifySession(session, handler.Type, notification);
-                }
-            }
-        }
-
-        public void AddPollHandler<TNotification>(PollHandler<TNotification> handler)
-            where TNotification : BaseObject
-        {
-            Handlers.Add(handler);
-        }
-        public void AddPollHandler<TNotification>(PollHandler<TNotification>.PollEvent poll)
-            where TNotification : BaseObject
-        {
-            Handlers.Add(new PollHandler<TNotification>(poll));
-        }
-
     }
 }

+ 34 - 7
InABox.Core/Notifications/Notify.cs

@@ -6,20 +6,47 @@ namespace InABox.Core
 {
     public class Notify
     {
-        public static Notifier? Notifier { get; set; }
+        private static List<IPollHandler> Handlers = new List<IPollHandler>();
+
+        private static List<Notifier> Notifiers { get; set; } = new List<Notifier>();
 
         private Notify() { }
 
+        public static void AddNotifier(Notifier notifier) =>
+            Notifiers.Add(notifier);
+
         public static void Push<TNotification>(TNotification notification) where TNotification : BaseObject => 
-            Notifier?.Push(notification);
+            Notifiers.ForEach(x => x.Push(notification));
 
-        public static void Push<TNotification>(Guid session, TNotification notification) where TNotification : BaseObject => 
-            Notifier?.Push(session, notification);
+        public static void Push<TNotification>(Guid session, TNotification notification) where TNotification : BaseObject =>
+            Notifiers.ForEach(x => x.Push(session, notification));
 
-        public static void PushUser<TNotification>(Guid userID, TNotification notification) where TNotification : BaseObject => 
-            Notifier?.PushUser(userID, notification);
+        public static void PushUser<TNotification>(Guid userID, TNotification notification) where TNotification : BaseObject =>
+            Notifiers.ForEach(x => x.PushUser(userID, notification));
 
         public static void Push<TNotification>(Platform platform, TNotification notification) where TNotification : BaseObject => 
-            Notifier?.Push(platform, notification);
+            Notifiers.ForEach(x => x.Push(platform, notification));
+
+        public static void Poll(Guid session)
+        {
+            foreach (var handler in Handlers)
+            {
+                foreach (var notification in handler.Poll(session))
+                {
+                    Notifiers.ForEach(x => x.Push(session, handler.Type, notification));
+                }
+            }
+        }
+
+        public static void AddPollHandler<TNotification>(PollHandler<TNotification> handler)
+            where TNotification : BaseObject
+        {
+            Handlers.Add(handler);
+        }
+        public static void AddPollHandler<TNotification>(PollHandler<TNotification>.PollEvent poll)
+            where TNotification : BaseObject
+        {
+            Handlers.Add(new PollHandler<TNotification>(poll));
+        }
     }
 }

+ 21 - 2
InABox.Core/Notifications/Platform.cs

@@ -6,8 +6,27 @@ namespace InABox.Core
 {
     public enum Platform
     {
-        Desktop,
         Mobile,
-        Other
+        Desktop,
+        DatabaseEngine,
+        WebEngine,
+        GPSEngine,
+        SchedulerEngine,
+        Server
+    }
+
+    public static class PlatformUtils
+    {
+        public static string PlatformToString(Platform platform) => platform switch
+        {
+            Platform.Desktop => "Desktop",
+            Platform.Mobile => "Mobile",
+            Platform.DatabaseEngine => "Database",
+            Platform.WebEngine => "Web Engine",
+            Platform.GPSEngine => "GPS Engine",
+            Platform.SchedulerEngine => "Scheduler",
+            Platform.Server => "Server",
+            _ => platform.ToString()
+        };
     }
 }

+ 3 - 0
InABox.Core/User/User.cs

@@ -66,6 +66,8 @@ namespace InABox.Core
         [CheckBoxEditor(Visible = Visible.Default)]
         public bool Disabled { get; set; }
 
+        public UserPlatform Platform { get; set; }
+
         [EditorSequence("2FA", 1)]
         [CheckBoxEditor]
         public bool Use2FA { get; set; }
@@ -135,6 +137,7 @@ namespace InABox.Core
         {
             base.Init();
             SecurityGroup = new SecurityGroupLink();
+            Platform = new UserPlatform();
         }
 
         public override string ToString()

+ 15 - 0
InABox.Core/User/UserPlatform.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace InABox.Core
+{
+    public class UserPlatform : EnclosedEntity
+    {
+        [TextBoxEditor(Visible = Visible.Optional, Editable = Editable.Hidden)]
+        public string DesktopVersion { get; set; }
+
+        [TextBoxEditor(Visible = Visible.Optional, Editable = Editable.Hidden)]
+        public string MobileVersion { get; set; }
+    }
+}

+ 3 - 3
InABox.Database/DbFactory.cs

@@ -314,7 +314,7 @@ namespace InABox.Database
             }
         }
 
-        public static IStore<TEntity> FindStore<TEntity>(Guid userguid, string userid, string platform, string version) 
+        public static IStore<TEntity> FindStore<TEntity>(Guid userguid, string userid, Platform platform, string version) 
             where TEntity : Entity, new()
         {
             var defType = typeof(Store<>).MakeGenericType(typeof(TEntity));
@@ -332,7 +332,7 @@ namespace InABox.Database
 
         private static CoreTable DoQueryMultipleQuery<TEntity>(
             IQueryDef query,
-            Guid userguid, string userid, string platform, string version) 
+            Guid userguid, string userid, Platform platform, string version) 
             where TEntity : Entity, new()
         {
             var store = FindStore<TEntity>(userguid, userid, platform, version);
@@ -341,7 +341,7 @@ namespace InABox.Database
 
         public static Dictionary<string, CoreTable> QueryMultiple(
             Dictionary<string, IQueryDef> queries, 
-            Guid userguid, string userid, string platform, string version)
+            Guid userguid, string userid, Platform platform, string version)
         {
             var result = new Dictionary<string, CoreTable>();
 

+ 1 - 1
InABox.Database/Stores/IStore.cs

@@ -9,7 +9,7 @@ namespace InABox.Database
         Guid UserGuid { get; set; }
         string UserID { get; set; }
 
-        string Platform { get; set; }
+        Platform Platform { get; set; }
         string Version { get; set; }
 
         IProvider Provider { get; set; }

+ 1 - 1
InABox.Database/Stores/Store.cs

@@ -26,7 +26,7 @@ namespace InABox.Database
         public Guid UserGuid { get; set; }
         public string UserID { get; set; }
 
-        public string Platform { get; set; }
+        public Platform Platform { get; set; }
         public string Version { get; set; }
         public IProvider Provider { get; set; }
 

+ 152 - 26
InABox.Server/IPC/PipeIPCServer.cs

@@ -1,13 +1,14 @@
 using H.Formatters;
 using H.Pipes;
 using H.Pipes.AccessControl;
+using H.Pipes.Args;
 using InABox.API;
 using InABox.Clients;
 using InABox.Core;
 using InABox.IPC.Shared;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Microsoft.CodeAnalysis.Operations;
+using InABox.Server.WebSocket;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO.Pipes;
@@ -21,14 +22,116 @@ namespace Piping
 {
     using PipeResponse = PipeRequest;
 
+    delegate void PipePollEvent(PipeNotifyState.Session session);
+
+    class PipeNotifyState
+    {
+        public class Session
+        {
+            public PipeConnection<PipeResponse?> Connection { get; }
+
+            public Guid SessionID { get; }
+
+            public Platform Platform { get; }
+
+            public Session(PipeConnection<PipeResponse?> connection, Guid sessionID, Platform platform)
+            {
+                Connection = connection;
+                SessionID = sessionID;
+                Platform = platform;
+            }
+        }
+
+        public ConcurrentDictionary<Guid, Session> SessionMap = new();
+
+        public event PipePollEvent? OnPoll;
+
+        public void Poll(Session session)
+        {
+            OnPoll?.Invoke(session);
+        }
+    }
+
+    class PipeIPCNotifier : Notifier
+    {
+        PipeNotifyState NotifyState { get; set; }
+
+        public PipeIPCNotifier(PipeNotifyState notifyState)
+        {
+            NotifyState = notifyState;
+            NotifyState.OnPoll += NotifyState_OnPoll;
+        }
+
+        private void NotifyState_OnPoll(PipeNotifyState.Session session)
+        {
+            Notify.Poll(session.SessionID);
+        }
+
+        protected override IEnumerable<Guid> GetSessions(Platform platform)
+        {
+            return NotifyState.SessionMap.Where(x => x.Value.Platform == platform).Select(x => x.Key);
+        }
+
+        protected override IEnumerable<Guid> GetUserSessions(Guid userID)
+        {
+            return CredentialsCache.GetUserSessions(userID);
+        }
+
+        protected override void NotifyAll<TNotification>(TNotification notification)
+        {
+            foreach(var session in NotifyState.SessionMap.Values)
+            {
+                session.Connection.WriteAsync(PipeRequest.Notification(notification)).ContinueWith(task =>
+                {
+                    if(task.Exception != null)
+                    {
+                        Logger.Send(LogType.Error, "", $"Error in notification: {CoreUtils.FormatException(task.Exception)}");
+                    }
+                });
+            }
+        }
+
+        protected override void NotifySession<TNotification>(Guid sessionID, TNotification notification)
+        {
+            if(NotifyState.SessionMap.TryGetValue(sessionID, out var session))
+            {
+                session.Connection.WriteAsync(PipeRequest.Notification(notification)).ContinueWith(task =>
+                {
+                    if (task.Exception != null)
+                    {
+                        Logger.Send(LogType.Error, "", $"Error in notification: {CoreUtils.FormatException(task.Exception)}");
+                    }
+                });
+            }
+        }
+
+        protected override void NotifySession(Guid sessionID, Type TNotification, BaseObject notification)
+        {
+            if (NotifyState.SessionMap.TryGetValue(sessionID, out var session))
+            {
+                session.Connection.WriteAsync(PipeRequest.Notification(TNotification, notification)).ContinueWith(task =>
+                {
+                    if (task.Exception != null)
+                    {
+                        Logger.Send(LogType.Error, "", $"Error in notification: {CoreUtils.FormatException(task.Exception)}");
+                    }
+                });
+            }
+        }
+    }
+
     public class PipeIPCServer : IDisposable
     {
         PipeServer<PipeRequest> Server;
 
+        PipeNotifyState NotifyState = new();
+
         public PipeIPCServer(string name)
         {
             Server = new PipeServer<PipeRequest>(name);
 
+            Notify.AddNotifier(new PipeIPCNotifier(NotifyState));
+
             var pipeSecurity = new PipeSecurity();
             pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSid, null), PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow));
             pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null), PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow));
@@ -91,76 +194,95 @@ namespace Piping
             };
         }
 
-        private static PipeResponse QueryMultiple(PipeRequest request)
+        private class RequestData
+        {
+            public ConnectionMessageEventArgs<PipeRequest?> e { get; }
+
+            public RequestData(ConnectionMessageEventArgs<PipeResponse?> e)
+            {
+                this.e = e;
+            }
+        }
+
+        private PipeResponse QueryMultiple(PipeRequest request, RequestData data)
         {
             var response = RestService.QueryMultiple(request.GetRequest<MultiQueryRequest>(), true);
             return request.Respond(response);
         }
 
-        private static PipeResponse Validate(PipeRequest request)
+        private PipeResponse Validate(PipeRequest request, RequestData data)
         {
-            var response = RestService.Validate(request.GetRequest<ValidateRequest>());
+            var validateRequest = request.GetRequest<ValidateRequest>();
+            var response = RestService.Validate(validateRequest);
+
+            if(response.Session != Guid.Empty)
+            {
+                var newSession = new PipeNotifyState.Session(data.e.Connection, response.Session, validateRequest.Credentials.Platform);
+                NotifyState.SessionMap[response.Session] = newSession;
+                NotifyState.Poll(newSession);
+            }
+
             return request.Respond(response);
         }
 
-        private static PipeResponse Ping(PipeRequest request) => request.Respond(new PingResponse().Status(StatusCode.OK));
+        private PipeResponse Ping(PipeRequest request, RequestData data) => request.Respond(new PingResponse().Status(StatusCode.OK));
 
-        private static PipeResponse Info(PipeRequest request)
+        private PipeResponse Info(PipeRequest request, RequestData data)
         {
             var response = RestService.Info(request.GetRequest<InfoRequest>());
             return request.Respond(response);
         }
         
-        private static PipeResponse Check2FA(PipeRequest request)
+        private PipeResponse Check2FA(PipeRequest request, RequestData data)
         {
             var response = RestService.Check2FA(request.GetRequest<Check2FARequest>());
             return request.Respond(response);
         }
 
-        private static PipeResponse Query<T>(PipeRequest request) where T : Entity, new()
+        private PipeResponse Query<T>(PipeRequest request, RequestData data) where T : Entity, new()
         {
             var response = RestService<T>.List(request.GetRequest<QueryRequest<T>>());
             return request.Respond(response);
         }
 
-        private static PipeResponse Save<T>(PipeRequest request) where T : Entity, new()
+        private PipeResponse Save<T>(PipeRequest request, RequestData data) where T : Entity, new()
         {
             var response = RestService<T>.Save(request.GetRequest<SaveRequest<T>>());
             return request.Respond(response);
         }
-        private static PipeResponse MultiSave<T>(PipeRequest request) where T : Entity, new()
+        private PipeResponse MultiSave<T>(PipeRequest request, RequestData data) where T : Entity, new()
         {
             var response = RestService<T>.MultiSave(request.GetRequest<MultiSaveRequest<T>>());
             return request.Respond(response);
         }
 
-        private static PipeResponse Delete<T>(PipeRequest request) where T : Entity, new()
+        private PipeResponse Delete<T>(PipeRequest request, RequestData data) where T : Entity, new()
         {
             var response = RestService<T>.Delete(request.GetRequest<DeleteRequest<T>>());
             return request.Respond(response);
         }
-        private static PipeResponse MultiDelete<T>(PipeRequest request) where T : Entity, new()
+        private PipeResponse MultiDelete<T>(PipeRequest request, RequestData data) where T : Entity, new()
         {
             var response = RestService<T>.MultiDelete(request.GetRequest<MultiDeleteRequest<T>>());
             return request.Respond(response);
         }
 
-        private static MethodInfo QueryMethod = GetMethod(nameof(Query));
-        private static MethodInfo SaveMethod = GetMethod(nameof(Save));
-        private static MethodInfo MultiSaveMethod = GetMethod(nameof(MultiSave));
-        private static MethodInfo DeleteMethod = GetMethod(nameof(Delete));
-        private static MethodInfo MultiDeleteMethod = GetMethod(nameof(MultiDelete));
-        private static MethodInfo QueryMultipleMethod = GetMethod(nameof(QueryMultiple));
-        private static MethodInfo ValidateMethod = GetMethod(nameof(Validate));
-        private static MethodInfo Check2FAMethod = GetMethod(nameof(Check2FA));
-        private static MethodInfo PingMethod = GetMethod(nameof(Ping));
-        private static MethodInfo InfoMethod = GetMethod(nameof(Info));
+        private static readonly MethodInfo QueryMethod = GetMethod(nameof(Query));
+        private static readonly MethodInfo SaveMethod = GetMethod(nameof(Save));
+        private static readonly MethodInfo MultiSaveMethod = GetMethod(nameof(MultiSave));
+        private static readonly MethodInfo DeleteMethod = GetMethod(nameof(Delete));
+        private static readonly MethodInfo MultiDeleteMethod = GetMethod(nameof(MultiDelete));
+        private static readonly MethodInfo QueryMultipleMethod = GetMethod(nameof(QueryMultiple));
+        private static readonly MethodInfo ValidateMethod = GetMethod(nameof(Validate));
+        private static readonly MethodInfo Check2FAMethod = GetMethod(nameof(Check2FA));
+        private static readonly MethodInfo PingMethod = GetMethod(nameof(Ping));
+        private static readonly MethodInfo InfoMethod = GetMethod(nameof(Info));
 
         private static MethodInfo GetMethod(string name) =>
-            typeof(PipeIPCServer).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static)
+            typeof(PipeIPCServer).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Instance)
             ?? throw new Exception($"Invalid method '{name}'");
 
-        private void Server_MessageReceived(object? sender, H.Pipes.Args.ConnectionMessageEventArgs<PipeRequest?> e)
+        private void Server_MessageReceived(object? sender, ConnectionMessageEventArgs<PipeRequest?> e)
         {
             Task.Run(() =>
             {
@@ -190,7 +312,7 @@ namespace Piping
                         method = method.MakeGenericMethod(entityType);
                     }
 
-                    var response = method.Invoke(null, new object[] { e.Message }) as PipeResponse;
+                    var response = method.Invoke(this, new object[] { e.Message, new RequestData(e) }) as PipeResponse;
                     e.Connection.WriteAsync(response);
                 }
                 catch (Exception err)
@@ -214,6 +336,10 @@ namespace Piping
         private void Server_ClientDisconnected(object? sender, H.Pipes.Args.ConnectionEventArgs<PipeRequest> e)
         {
             Logger.Send(LogType.Information, "", "Client Disconnected");
+
+            var sessionID = NotifyState.SessionMap.Where(x => x.Value.Connection == e.Connection).FirstOrDefault().Key;
+            NotifyState.SessionMap.TryRemove(sessionID, out var session);
+
             e.Connection.DisposeAsync();
         }
 

+ 3 - 3
InABox.Server/Rest/RestListener.cs

@@ -471,7 +471,7 @@ namespace InABox.API
 
         private void SocketServer_Poll(NotifyState.Session session)
         {
-            Poll(session.SessionID);
+            Notify.Poll(session.SessionID);
         }
 
         public void Start()
@@ -489,7 +489,7 @@ namespace InABox.API
             SocketServer.Push(notification);
         }
 
-        protected override void NotifySession(Guid session, Type TNotification, object? notification)
+        protected override void NotifySession(Guid session, Type TNotification, BaseObject notification)
         {
             SocketServer.Push(session, TNotification, notification);
         }
@@ -566,7 +566,7 @@ namespace InABox.API
             if(webSocketPort != 0)
             {
                 notifier = new RestNotifier(webSocketPort);
-                Notify.Notifier = notifier;
+                Notify.AddNotifier(notifier);
             }
 
             host = Host.Create();

+ 7 - 1
inabox.client.ipc/IPCClient.cs

@@ -22,10 +22,16 @@ namespace InABox.Client.IPC
         public delegate void ConnectEvent();
         public delegate void DisconnectEvent();
 
+        /// <summary>
+        /// A handler for any requests pushed from the server, i.e., not initialised by the client.
+        /// </summary>
+        public delegate void PushEvent(PipeRequest request);
+
         public bool Disconnected { get; private set; }
 
         public event ConnectEvent? OnConnect;
         public event DisconnectEvent? OnDisconnect;
+        public event PushEvent? OnPush;
 
         public IPCClient(string name)
         {
@@ -95,7 +101,7 @@ namespace InABox.Client.IPC
             }
             else
             {
-                Responses[e.Message.RequestID] = e.Message;
+                OnPush?.Invoke(e.Message);
             }
         }
 

+ 16 - 1
inabox.client.ipc/IPCClientFactory.cs

@@ -1,4 +1,7 @@
-using System;
+using InABox.Clients;
+using InABox.Core;
+using InABox.IPC.Shared;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
@@ -16,8 +19,20 @@ namespace InABox.Client.IPC
             {
                 client = new IPCClient(pipeName);
                 Clients[pipeName] = client;
+                client.OnPush += Client_OnPush;
             }
             return client;
         }
+
+        private static void Client_OnPush(PipeRequest request)
+        {
+            if(request.Method == Method.Notification)
+            {
+                if(request.Type is not null && CoreUtils.TryGetEntity(request.Type, out var entity))
+                {
+                    ClientFactory.Notifications.Notify(entity, Serialization.Deserialize(entity, request.Data));
+                }
+            }
+        }
     }
 }

+ 1 - 2
inabox.client.ipc/PipeIPCClient.cs

@@ -153,7 +153,7 @@ namespace InABox.Client.IPC
             }
         }
 
-        protected override TEntity[] DoLoad(Filter<TEntity> filter = null, SortOrder<TEntity> sort = null)
+        protected override TEntity[] DoLoad(Filter<TEntity>? filter = null, SortOrder<TEntity>? sort = null)
         {
             var request = new QueryRequest<TEntity>
             {
@@ -379,7 +379,6 @@ namespace InABox.Client.IPC
                         response.Session,
                         response.Recipient2FA,
                         response.PasswordExpiration
-
                     );
                 }
                 else if (response.Status == StatusCode.BadServer)

+ 1 - 1
inabox.client.rest/InABox.Client.Rest/RestClientCache.cs

@@ -30,7 +30,7 @@ namespace InABox.Clients
             
         private static bool Check(String host, bool https)
         {
-            var uri = new Uri(host);
+            var uri = new Uri($"http://{host}");
             string schema = https ? "https" : "http";
             var url = $"{schema}://{uri.Host}:{uri.Port}";
             var req = new RestRequest("/info", Method.GET) { Timeout = 20000 };

+ 4 - 2
inabox.client.rest/InABox.Client.Rest/SocketClientCache.cs

@@ -8,8 +8,10 @@ namespace InABox.Clients
 
         public static void StartWebSocket(string url, int port, Guid session)
         {
-            Uri uri = new Uri(url);
-            var key = $"{uri.Host}${session}";
+            var host = url.Split(new[] { "://" }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
+
+            Uri uri = new Uri($"http://{host}");
+            var key = $"{uri.Host}:{uri.Port}${session}";
             if (!Clients.ContainsKey(key))
                 Clients[key] = new WebSocketClient(uri.Host, port, session);
         }

+ 1 - 1
inabox.client.websocket/WebSocketClient.cs

@@ -52,7 +52,7 @@ namespace InABox.Client.WebSocket
         {
             Logger.Send(LogType.Information, "", "WebSocket connected to server");
 
-            var initial = new InitialMessage(Session, Platform.Other);
+            var initial = new InitialMessage(Session, ClientFactory.Platform);
             Socket.Send(initial.WriteToBytes());
         }
 

+ 9 - 1
inabox.ipc.shared/PipeRequest.cs

@@ -29,7 +29,8 @@ namespace InABox.IPC.Shared
         Validate,
         Check2FA,
         Ping,
-        Info
+        Info,
+        Notification
     }
 
     [Serializable]
@@ -141,5 +142,12 @@ namespace InABox.IPC.Shared
         {
             return new PipeRequest(Guid.NewGuid(), Method.Info, null, Serialization.Serialize(request));
         }
+
+        public static PipeRequest Notification<TNotification>(TNotification notification) where TNotification : BaseObject
+            => Notification(typeof(TNotification), notification);
+        public static PipeRequest Notification(Type TNotification, BaseObject notification)
+        {
+            return new PipeRequest(Guid.NewGuid(), Method.Notification, TNotification.EntityName(), Serialization.Serialize(notification));
+        }
     }
 }