From 2ca092b0175f46bff402462401f1daa5445f56ce Mon Sep 17 00:00:00 2001 From: olcxja Date: Sat, 22 Aug 2026 12:36:54 +0200 Subject: [PATCH] voice chat [TEST] --- LarpixServer/Account/Requests.cs | 35 ++++- LarpixServer/Account/Utils.cs | 63 +++------ LarpixServer/Room/Shared.cs | 128 ++++++++++++++++++ .../Utils/AppJsonSerializerContext.cs | 1 + LarpixServer/Utils/Utils.cs | 42 +++++- LarpixVoice/Utils.cs | 8 +- 6 files changed, 226 insertions(+), 51 deletions(-) diff --git a/LarpixServer/Account/Requests.cs b/LarpixServer/Account/Requests.cs index 0759149..5ad2fe4 100644 --- a/LarpixServer/Account/Requests.cs +++ b/LarpixServer/Account/Requests.cs @@ -1038,6 +1038,24 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec , password)); break; } + case "room/voice/join": + await context.Response.WriteAsync( + Encryption.Encryption.EncryptString( + await Room.Shared.VoiceJoin(id, serializedBody.string2) + , password)); + break; + case "room/voice/leave": + await context.Response.WriteAsync( + Encryption.Encryption.EncryptString( + await Room.Shared.VoiceLeave(id, serializedBody.string2) + , password)); + break; + case "room/signal": + await context.Response.WriteAsync( + Encryption.Encryption.EncryptString( + await Room.Shared.RoomSignal(id, serializedBody.string2) + , password)); + break; case "group/message/react/remove": { Universal3String msgReact = JsonSerializer.Deserialize( @@ -1166,10 +1184,19 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec return; } - await context.Response.WriteAsync( - Encryption.Encryption.EncryptString( - await Utils.UpdateUserPublicStorageEntryRaw(id, target, fileBytes) - , password)); + SemaphoreSlim userLock = Utils.GetUserLock(id); + await userLock.WaitAsync(); + try + { + await context.Response.WriteAsync( + Encryption.Encryption.EncryptString( + await Utils.UpdateUserPublicStorageEntryRaw(id, target, fileBytes) + , password)); + } + finally + { + userLock.Release(); + } } public static async Task UploadDmAttachmentStaging(HttpContext context, Func next, IQueryCollection query) { diff --git a/LarpixServer/Account/Utils.cs b/LarpixServer/Account/Utils.cs index f0da0f6..f5a1db7 100644 --- a/LarpixServer/Account/Utils.cs +++ b/LarpixServer/Account/Utils.cs @@ -379,24 +379,15 @@ public class Utils string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public"; string path = $"{dirPath}/{entry}"; - SemaphoreSlim userLock = GetUserLock(id); - await userLock.WaitAsync(); - try - { - long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); - long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; + long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); + long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; - if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT) - { - return "error:storage.limit.exceeded"; - } - - await Fs.WriteFile(path, contentBytes); - } - finally + if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT) { - userLock.Release(); + return "error:storage.limit.exceeded"; } + + await Fs.WriteFile(path, contentBytes); return "success:storage.entry.updated"; } @@ -418,24 +409,15 @@ public class Utils string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public"; string path = $"{dirPath}/{entry}"; - SemaphoreSlim userLock = GetUserLock(id); - await userLock.WaitAsync(); - try - { - long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); - long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; + long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); + long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; - if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT) - { - return "error:storage.limit.exceeded"; - } - - await Fs.WriteFile(path, contentBytes); - } - finally + if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT) { - userLock.Release(); + return "error:storage.limit.exceeded"; } + + await Fs.WriteFile(path, contentBytes); return "success:storage.entry.updated"; } @@ -471,24 +453,15 @@ public class Utils string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/private"; string path = $"{dirPath}/{entry}"; - SemaphoreSlim userLock = GetUserLock(id); - await userLock.WaitAsync(); - try - { - long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); - long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; + long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); + long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; - if (currentTotalSize - fileExistingSize + contentBytes.Length > PRIVATE_STORAGE_LIMIT) - { - return "error:storage.limit.exceeded"; - } - - await Fs.WriteFile(path, contentBytes); - } - finally + if (currentTotalSize - fileExistingSize + contentBytes.Length > PRIVATE_STORAGE_LIMIT) { - userLock.Release(); + return "error:storage.limit.exceeded"; } + + await Fs.WriteFile(path, contentBytes); return "success:storage.entry.updated"; } diff --git a/LarpixServer/Room/Shared.cs b/LarpixServer/Room/Shared.cs index 74c9442..ed0d90f 100644 --- a/LarpixServer/Room/Shared.cs +++ b/LarpixServer/Room/Shared.cs @@ -600,4 +600,132 @@ public class Shared if (roomType == "groups") return "group"; return "room"; } + + public static async Task VoiceJoin(string id, string body) + { + Universal2String serializedBody = JsonSerializer.Deserialize( + body, + AppJsonSerializerContext.Default.Universal2String + ); + string roomType = serializedBody.string1; + string roomId = serializedBody.string2; + + if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden"; + + var callList = ActiveCalls.GetOrAdd(roomId, _ => new List()); + lock (callList) + { + if (!callList.Contains(id)) + { + callList.Add(id); + } + } + + string callState; + lock (callList) { callState = JsonSerializer.Serialize(callList, AppJsonSerializerContext.Default.ListString); } + + string bestIp = ""; + long bestLoad = long.MaxValue; + + foreach (var ip in VOICE_IPS) + { + try + { + using var client = new System.Net.Http.HttpClient(); + client.Timeout = TimeSpan.FromSeconds(2); + string url = ip.StartsWith("http") ? ip : $"http://{ip}"; + string loadStr = await client.GetStringAsync($"{url}/load"); + if (long.TryParse(loadStr, out long load)) + { + if (load < bestLoad) + { + bestLoad = load; + bestIp = ip; + } + } + } + catch {} + } + + if (string.IsNullOrEmpty(bestIp)) + { + if (VOICE_IPS.Count > 0) bestIp = VOICE_IPS.First(); + else return "error:no.voice.servers"; + } + + string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); + foreach(string memberWD in members) + { + if (Account.Utils.IsUserLocal(memberWD, out string _)) + { + string memberId = Account.Utils.GetValidIdOrZero(memberWD); + _ = Utils.Utils.SendWsMessage(memberId, $"call_update:{roomId}:{callState}"); + } + } + return $"success:{bestIp}"; + } + + public static async Task VoiceLeave(string id, string body) + { + Universal2String serializedBody = JsonSerializer.Deserialize( + body, + AppJsonSerializerContext.Default.Universal2String + ); + string roomType = serializedBody.string1; + string roomId = serializedBody.string2; + + if (ActiveCalls.TryGetValue(roomId, out var callList)) + { + bool removed = false; + lock (callList) + { + if (callList.Contains(id)) + { + callList.Remove(id); + removed = true; + } + } + + if (removed) + { + string callState; + lock (callList) { callState = JsonSerializer.Serialize(callList, AppJsonSerializerContext.Default.ListString); } + + string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); + foreach(string memberWD in members) + { + if (Account.Utils.IsUserLocal(memberWD, out string _)) + { + string memberId = Account.Utils.GetValidIdOrZero(memberWD); + _ = Utils.Utils.SendWsMessage(memberId, $"call_update:{roomId}:{callState}"); + } + } + } + } + return "success"; + } + + public static async Task RoomSignal(string id, string body) + { + Universal3String serializedBody = JsonSerializer.Deserialize( + body, + AppJsonSerializerContext.Default.Universal3String + ); + string roomType = serializedBody.string1; + string roomId = serializedBody.string2; + string signalPayload = serializedBody.string3; + + if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden"; + + string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); + foreach(string memberWD in members) + { + if (Account.Utils.IsUserLocal(memberWD, out string _)) + { + string memberId = Account.Utils.GetValidIdOrZero(memberWD); + _ = Utils.Utils.SendWsMessage(memberId, $"signal:{roomId}:{signalPayload}"); + } + } + return "success"; + } } diff --git a/LarpixServer/Utils/AppJsonSerializerContext.cs b/LarpixServer/Utils/AppJsonSerializerContext.cs index 4b57a72..bab819e 100644 --- a/LarpixServer/Utils/AppJsonSerializerContext.cs +++ b/LarpixServer/Utils/AppJsonSerializerContext.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(GroupKeyRotationPayload))] [JsonSerializable(typeof(string[]))] +[JsonSerializable(typeof(List))] public partial class AppJsonSerializerContext : JsonSerializerContext { } diff --git a/LarpixServer/Utils/Utils.cs b/LarpixServer/Utils/Utils.cs index b94df74..2a55726 100644 --- a/LarpixServer/Utils/Utils.cs +++ b/LarpixServer/Utils/Utils.cs @@ -105,6 +105,7 @@ public class Utils }; public static ConcurrentDictionary> ActiveWebSockets = new(); + public static ConcurrentDictionary> ActiveCalls = new(); public static async Task WsHandler(HttpContext context, System.Net.WebSockets.WebSocket webSocket, IQueryCollection query) { @@ -168,6 +169,39 @@ public class Utils { await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } + + // Check if user was in any call and remove them + foreach (var kvp in ActiveCalls) + { + bool removed = false; + lock (kvp.Value) + { + if (kvp.Value.Contains(id)) + { + kvp.Value.Remove(id); + removed = true; + } + } + + if (removed) + { + string callState; + lock (kvp.Value) { callState = JsonSerializer.Serialize(kvp.Value, AppJsonSerializerContext.Default.ListString); } + + // Broadcast to all room members + string[] members = Fs.ReadDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{kvp.Key}/members"); + if (members.Length == 0) members = Fs.ReadDirectory($"{ROOMS_DIR}/groups/{DOMAIN}/{kvp.Key}/members"); + + foreach (string memberWD in members) + { + if (Account.Utils.IsUserLocal(memberWD, out string _)) + { + string memberId = Account.Utils.GetValidIdOrZero(memberWD); + _ = SendWsMessage(memberId, $"call_update:{kvp.Key}:{callState}"); + } + } + } + } } } @@ -265,7 +299,13 @@ public class Utils if (parts.Length == 2) { var key = parts[0].Trim(); - var val = parts[1].Trim(); + var val = parts[1]; + int commentIndex = val.IndexOf('#'); + if (commentIndex >= 0) + { + val = val.Substring(0, commentIndex); + } + val = val.Trim(); Environment.SetEnvironmentVariable(key, val); } diff --git a/LarpixVoice/Utils.cs b/LarpixVoice/Utils.cs index 69242b7..a8c109f 100644 --- a/LarpixVoice/Utils.cs +++ b/LarpixVoice/Utils.cs @@ -20,7 +20,13 @@ public class Utils if (parts.Length == 2) { var key = parts[0].Trim(); - var val = parts[1].Trim(); + var val = parts[1]; + int commentIndex = val.IndexOf('#'); + if (commentIndex >= 0) + { + val = val.Substring(0, commentIndex); + } + val = val.Trim(); Environment.SetEnvironmentVariable(key, val); }