diff --git a/LarpixServer/Account/Requests.cs b/LarpixServer/Account/Requests.cs index 1d17b6e..9062cf2 100644 --- a/LarpixServer/Account/Requests.cs +++ b/LarpixServer/Account/Requests.cs @@ -647,6 +647,12 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec case "user/dm/create": await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Requests.DmCreate(id, serializedBody.string2), password)); break; + case "dm/message/send": + await context.Response.WriteAsync( + Encryption.Encryption.EncryptString( + await Room.Requests.DmMessageSend(id, serializedBody.string2) + , password)); + break; case "dm/messages/get": { Universal3String msgGet = JsonSerializer.Deserialize( diff --git a/LarpixServer/Program.cs b/LarpixServer/Program.cs index 39586e0..fed657b 100644 --- a/LarpixServer/Program.cs +++ b/LarpixServer/Program.cs @@ -58,6 +58,17 @@ public class Program switch (path) { + case "/_larpix/ws": + if (context.WebSockets.IsWebSocketRequest) + { + using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await WsHandler(context, webSocket, query); + } + else + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + } + return; case "/_larpix/serverinfo": ServerInfo serverInfo = new ServerInfo(); serverInfo.domain = DOMAIN; diff --git a/LarpixServer/Room/Requests.cs b/LarpixServer/Room/Requests.cs index 3d55c86..a3b9e80 100644 --- a/LarpixServer/Room/Requests.cs +++ b/LarpixServer/Room/Requests.cs @@ -133,25 +133,28 @@ public class Requests { return "error:cant.invite.urself"; } - SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2); - await userLock2.WaitAsync(); - - try + string inviteFile = ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/recv/{id}"; + if (Fs.Exists(inviteFile)) { - string inviteFile = ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/recv/{id}"; - if (Fs.Exists(inviteFile)) - { - return "info:user.already.invited"; - } + return "info:user.already.invited"; + } - byte[] timestamp = Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()); - await Fs.WriteFile(inviteFile, timestamp); - await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{id2}", timestamp); //we should also save that this user invited someone, because listing sent invites is cool - } - finally + byte[] timestamp = Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()); + await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{id2}", timestamp); //we should also save that this user invited someone, because listing sent invites is cool + + _ = Task.Run(async () => { - userLock2.Release(); - } + SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2); + await userLock2.WaitAsync(); + try + { + await Fs.WriteFile(inviteFile, timestamp); + } + finally + { + userLock2.Release(); + } + }); return "success:user.invited"; } @@ -170,17 +173,21 @@ public class Requests string id2 = tId; if (id2 == "0" || string.IsNullOrEmpty(id2)) return "error:user.not.found"; - SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2); - await userLock2.WaitAsync(); - try + Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{id2}"); + + _ = Task.Run(async () => { - Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/recv/{id}"); - Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{id2}"); - } - finally - { - userLock2.Release(); - } + SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2); + await userLock2.WaitAsync(); + try + { + Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/recv/{id}"); + } + finally + { + userLock2.Release(); + } + }); return "success:invite.revoked"; } @@ -199,17 +206,21 @@ public class Requests string id2 = tId; if (id2 == "0" || string.IsNullOrEmpty(id2)) return "error:user.not.found"; - SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2); - await userLock2.WaitAsync(); - try + Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/recv/{id2}"); + + _ = Task.Run(async () => { - Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/recv/{id2}"); - Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/sent/{id}"); - } - finally - { - userLock2.Release(); - } + SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2); + await userLock2.WaitAsync(); + try + { + Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/sent/{id}"); + } + finally + { + userLock2.Release(); + } + }); return "success:invite.declined"; } @@ -309,17 +320,20 @@ public class Requests if (isUserLocal) { - SemaphoreSlim userLock = Account.Utils.GetUserLock(id2); - await userLock.WaitAsync(); - try + _ = Task.Run(async () => { - await Account.Utils.UpdateUserDm(id2, dmId, "false", - startingMessage.timestamp); - } - finally - { - userLock.Release(); - } + SemaphoreSlim userLock = Account.Utils.GetUserLock(id2); + await userLock.WaitAsync(); + try + { + await Account.Utils.UpdateUserDm(id2, dmId, "false", + startingMessage.timestamp); + } + finally + { + userLock.Release(); + } + }); } else //federacja { @@ -344,4 +358,70 @@ public class Requests } return "error:cant.create.dm.without.invitation"; } + + public static async Task DmMessageSend(string id, string body) + { + Universal3String serializedBody = JsonSerializer.Deserialize( + body, + AppJsonSerializerContext.Default.Universal3String + ); + string dmId = serializedBody.string1; + string content = serializedBody.string2; + string attachment = serializedBody.string3; + + if (!await IsMemberOfDm(id, dmId)) return "error:forbidden"; + + string msgDir = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/messages"; + long last = 0; + if (Fs.Exists($"{msgDir}/last")) + { + last = long.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{msgDir}/last"))); + } + long nextId = last + 1; + + Message msg = new Message(); + msg.author = id; + msg.timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); + msg.type = "larp.text"; + msg.content = content; + msg.attachment = attachment; + msg.key = "0"; // Flag for encrypted content + msg.pervious = ""; + + await Fs.WriteFile($"{msgDir}/{nextId}", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg, AppJsonSerializerContext.Default.Message))); + await Fs.WriteFile($"{msgDir}/last", Encoding.UTF8.GetBytes(nextId.ToString())); + + string[] members = Fs.ReadDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/members"); + foreach(string memberWD in members) + { + bool isLocal = Account.Utils.IsUserLocal(memberWD, out string domain); + string memberId = Account.Utils.GetValidIdOrZero(memberWD); + if (isLocal) + { + if (memberId == id) + { + await Account.Utils.UpdateUserDm(memberId, dmId, "true", msg.timestamp); + await Utils.Utils.SendWsMessage(memberId, $"dm_message:{dmId}"); + } + else + { + _ = Task.Run(async () => + { + SemaphoreSlim userLock = Account.Utils.GetUserLock(memberId); + await userLock.WaitAsync(); + try + { + await Account.Utils.UpdateUserDm(memberId, dmId, "false", msg.timestamp); + } + finally + { + userLock.Release(); + } + await Utils.Utils.SendWsMessage(memberId, $"dm_message:{dmId}"); + }); + } + } + } + return "success:message.sent"; + } } \ No newline at end of file diff --git a/LarpixServer/Utils/Utils.cs b/LarpixServer/Utils/Utils.cs index 38929b0..b993045 100644 --- a/LarpixServer/Utils/Utils.cs +++ b/LarpixServer/Utils/Utils.cs @@ -1,5 +1,7 @@ using System.Collections.Concurrent; using System.Text; +using System.Text.Json; +using LarpixServer.Utils.Jsons; namespace LarpixServer.Utils; @@ -95,6 +97,100 @@ public class Utils ["ppt"] = "application/vnd.ms-powerpoint", ["pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation" }; + + public static ConcurrentDictionary> ActiveWebSockets = new(); + + public static async Task WsHandler(HttpContext context, System.Net.WebSockets.WebSocket webSocket, IQueryCollection query) + { + var buffer = new byte[8192]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + if (result.MessageType == System.Net.WebSockets.WebSocketMessageType.Close) + { + await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + return; + } + + string authMessage = Encoding.UTF8.GetString(buffer, 0, result.Count); + Universal2String authParams; + try { + authParams = JsonSerializer.Deserialize(authMessage, AppJsonSerializerContext.Default.Universal2String); + } catch { + await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.InvalidMessageType, "Invalid auth", CancellationToken.None); + return; + } + + string id = Account.Utils.GetValidIdOrZero(authParams.string1); + string password = await Account.Utils.GetPassword(id); + string secret = await Account.Utils.NonceDecryptBody(id, password, authParams.string2); + + string auth = await Account.Utils.Auth(id, password, secret); + + if (auth != Account.Utils.LOGIN_SUCCESS) + { + await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.PolicyViolation, "Unauthorized", CancellationToken.None); + return; + } + + ActiveWebSockets.AddOrUpdate(id, new List { webSocket }, (k, list) => { + lock(list) { + list.Add(webSocket); + } + return list; + }); + + try + { + while (webSocket.State == System.Net.WebSockets.WebSocketState.Open) + { + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + if (result.MessageType == System.Net.WebSockets.WebSocketMessageType.Close) + { + break; + } + } + } + catch { } + finally + { + if (ActiveWebSockets.TryGetValue(id, out var list)) + { + lock(list) { + list.Remove(webSocket); + } + } + if (webSocket.State == System.Net.WebSockets.WebSocketState.Open) + { + await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + } + } + + public static async Task SendWsMessage(string id, string message) + { + if (ActiveWebSockets.TryGetValue(id, out var list)) + { + List toRemove = null; + var bytes = Encoding.UTF8.GetBytes(message); + lock(list) { + foreach(var ws in list) + { + if (ws.State == System.Net.WebSockets.WebSocketState.Open) + { + _ = ws.SendAsync(new ArraySegment(bytes), System.Net.WebSockets.WebSocketMessageType.Text, true, CancellationToken.None); + } + else + { + if (toRemove == null) toRemove = new(); + toRemove.Add(ws); + } + } + if (toRemove != null) + { + foreach(var ws in toRemove) list.Remove(ws); + } + } + } + } public static async Task LoadBody(StreamReader reader) {