From 07ad37387c1d4a150ed623e9813483438a6397c0 Mon Sep 17 00:00:00 2001 From: Daniel Balk <67603460+Daniel-Balk@users.noreply.github.com> Date: Fri, 10 Mar 2023 18:04:41 +0100 Subject: [PATCH] Notification System (websocket) --- .../Notifications/ListenController.cs | 146 ++++++++++++++++++ .../App/Models/Notifications/BasicWSModel.cs | 9 ++ Moonlight/App/Models/Notifications/Login.cs | 6 + .../Models/Notifications/NotificationById.cs | 6 + .../NotificationClientService.cs | 11 +- .../NotificationServerService.cs | 11 +- Moonlight/Program.cs | 1 + 7 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 Moonlight/App/Http/Controllers/Api/Moonlight/Notifications/ListenController.cs create mode 100644 Moonlight/App/Models/Notifications/BasicWSModel.cs create mode 100644 Moonlight/App/Models/Notifications/Login.cs create mode 100644 Moonlight/App/Models/Notifications/NotificationById.cs diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/Notifications/ListenController.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/Notifications/ListenController.cs new file mode 100644 index 00000000..c539f4de --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/Notifications/ListenController.cs @@ -0,0 +1,146 @@ +using System.Net.WebSockets; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities.Notification; +using Moonlight.App.Models.Notifications; +using Moonlight.App.Repositories; +using Moonlight.App.Services; +using Moonlight.App.Services.Notifications; +using Moonlight.App.Services.Sessions; +using Newtonsoft.Json; + +namespace Moonlight.App.Http.Controllers.Api.Moonlight.Notifications; + +public class ListenController : ControllerBase +{ + internal WebSocket ws; + private bool active = true; + private bool isAuth = false; + private NotificationClient Client; + + private readonly IdentityService IdentityService; + private readonly NotificationRepository NotificationRepository; + private readonly OneTimeJwtService OneTimeJwtService; + private readonly NotificationClientService NotificationClientService; + private readonly NotificationServerService NotificationServerService; + + public ListenController(IdentityService identityService, + NotificationRepository notificationRepository, + OneTimeJwtService oneTimeJwtService, + NotificationClientService notificationClientService, + NotificationServerService notificationServerService) + { + IdentityService = identityService; + NotificationRepository = notificationRepository; + OneTimeJwtService = oneTimeJwtService; + NotificationClientService = notificationClientService; + NotificationServerService = notificationServerService; + } + + [Route("/api/moonlight/notifications/listen")] + public async Task Get() + { + if (HttpContext.WebSockets.IsWebSocketRequest) + { + using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); + ws = webSocket; + await Echo(); + } + else + { + HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden; + } + } + + private async Task Echo() + { + while (active) + { + byte[] bytes = new byte[1024 * 16]; + var asg = new ArraySegment(bytes); + var res = await ws.ReceiveAsync(asg, CancellationToken.None); + + var text = Encoding.UTF8.GetString(bytes).Trim('\0'); + + var obj = JsonConvert.DeserializeObject(text); + + if (!string.IsNullOrWhiteSpace(obj.Action)) + { + await HandleRequest(text, obj.Action); + } + + active = ws.State == WebSocketState.Open; + } + } + + private async Task HandleRequest(string text, string action) + { + if (!isAuth && action == "login") + await Login(text); + else if (!isAuth) + await ws.SendAsync(Encoding.UTF8.GetBytes("{\"error\": \"Unauthorised\"}"), WebSocketMessageType.Text, + WebSocketMessageFlags.EndOfMessage, CancellationToken.None); + else switch (action) + { + case "received": + await Received(text); + break; + case "read": + await Read(text); + break; + default: + break; + } + } + + private async Task Login(string json) + { + var jwt = JsonConvert.DeserializeObject(json).token; + + var dict = await OneTimeJwtService.Validate(jwt); + + if (dict == null) + { + string error = "{\"status\":false}"; + var bytes = Encoding.UTF8.GetBytes(error); + await ws.SendAsync(bytes, WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None); + return; + } + + var _clientId = dict["clientId"]; + var clientId = int.Parse(_clientId); + + var client = NotificationRepository.GetClients().Include(x => x.User).First(x => x.Id == clientId); + + Client = client; + await InitWebsocket(); + + string success = "{\"status\":true}"; + var byt = Encoding.UTF8.GetBytes(success); + await ws.SendAsync(byt, WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None); + } + + private async Task InitWebsocket() + { + NotificationClientService.listenController = this; + NotificationClientService.WebsocketReady(Client); + + isAuth = true; + } + + private async Task Received(string json) + { + var id = JsonConvert.DeserializeObject(json).notification; + + //TODO: Implement ws notification received + } + + private async Task Read(string json) + { + var id = JsonConvert.DeserializeObject(json).notification; + + await NotificationServerService.SendAction(NotificationClientService.User, + JsonConvert.SerializeObject(new NotificationById() {Action = "hide", notification = id})); + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Notifications/BasicWSModel.cs b/Moonlight/App/Models/Notifications/BasicWSModel.cs new file mode 100644 index 00000000..76e26896 --- /dev/null +++ b/Moonlight/App/Models/Notifications/BasicWSModel.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Notifications; + +public class BasicWSModel +{ + [JsonProperty("action")] + public string Action { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Notifications/Login.cs b/Moonlight/App/Models/Notifications/Login.cs new file mode 100644 index 00000000..2ed63d3e --- /dev/null +++ b/Moonlight/App/Models/Notifications/Login.cs @@ -0,0 +1,6 @@ +namespace Moonlight.App.Models.Notifications; + +public class Login : BasicWSModel +{ + public string token { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Notifications/NotificationById.cs b/Moonlight/App/Models/Notifications/NotificationById.cs new file mode 100644 index 00000000..727a3dcc --- /dev/null +++ b/Moonlight/App/Models/Notifications/NotificationById.cs @@ -0,0 +1,6 @@ +namespace Moonlight.App.Models.Notifications; + +public class NotificationById : BasicWSModel +{ + public int notification { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Notifications/NotificationClientService.cs b/Moonlight/App/Services/Notifications/NotificationClientService.cs index f59893e0..3e020eab 100644 --- a/Moonlight/App/Services/Notifications/NotificationClientService.cs +++ b/Moonlight/App/Services/Notifications/NotificationClientService.cs @@ -1,5 +1,8 @@ -using Moonlight.App.Database.Entities; +using System.Net.WebSockets; +using System.Text; +using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities.Notification; +using Moonlight.App.Http.Controllers.Api.Moonlight.Notifications; using Moonlight.App.Repositories; using Moonlight.App.Services.Sessions; @@ -9,6 +12,7 @@ public class NotificationClientService { private readonly NotificationRepository NotificationRepository; private readonly NotificationServerService NotificationServerService; + internal ListenController listenController; public NotificationClientService(NotificationRepository notificationRepository, NotificationServerService notificationServerService) { @@ -20,9 +24,10 @@ public class NotificationClientService public NotificationClient NotificationClient { get; set; } - public void SendAction(string action) + public async Task SendAction(string action) { - throw new NotImplementedException(); + await listenController.ws.SendAsync(Encoding.UTF8.GetBytes(action), WebSocketMessageType.Text, + WebSocketMessageFlags.EndOfMessage, CancellationToken.None); } public void WebsocketReady(NotificationClient client) diff --git a/Moonlight/App/Services/Notifications/NotificationServerService.cs b/Moonlight/App/Services/Notifications/NotificationServerService.cs index e8ecc7b3..92d76690 100644 --- a/Moonlight/App/Services/Notifications/NotificationServerService.cs +++ b/Moonlight/App/Services/Notifications/NotificationServerService.cs @@ -1,4 +1,5 @@ -using Moonlight.App.Database.Entities; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities.Notification; using Moonlight.App.Repositories; @@ -40,9 +41,9 @@ public class NotificationServerService return connectedClients.Where(x => x.User == user).ToList(); } - public void SendAction(User user, string action) + public async Task SendAction(User user, string action) { - var clients = NotificationRepository.GetClients().Where(x => x.User == user).ToList(); + var clients = NotificationRepository.GetClients().Include(x => x.User).Where(x => x.User == user).ToList(); foreach (var client in clients) { @@ -52,12 +53,12 @@ public class NotificationServerService NotificationClient = client }; - var connected = connectedClients.Where(x => x.NotificationClient == client).ToList(); + var connected = connectedClients.Where(x => x.NotificationClient.Id == client.Id).ToList(); if (connected.Count > 0) { var clientService = connected[0]; - clientService.SendAction(action); + await clientService.SendAction(action); } else { diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 36dce008..c2ad8f3d 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -137,6 +137,7 @@ namespace Moonlight app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.MapControllers();