From 97fbbc7b71f84a93ab33c34a9cb2247fbad449fb Mon Sep 17 00:00:00 2001 From: olcxja Date: Sun, 9 Aug 2026 16:51:07 +0200 Subject: [PATCH] group settings (TEST) --- LarpixServer/Account/Requests.cs | 17 +- LarpixServer/Room/Groups.cs | 180 +++++++++++++++++- .../Utils/AppJsonSerializerContext.cs | 1 + LarpixServer/Utils/Jsons.cs | 9 + 4 files changed, 194 insertions(+), 13 deletions(-) diff --git a/LarpixServer/Account/Requests.cs b/LarpixServer/Account/Requests.cs index a8b4cc7..8b7e840 100644 --- a/LarpixServer/Account/Requests.cs +++ b/LarpixServer/Account/Requests.cs @@ -666,7 +666,7 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec "dm/message/send", "dm/message/delete", "dm/message/react", "dm/key/update", "dm/message/pin/toggle", "dm/message/react/add", "dm/message/react/remove", "user/storage/public/update", "user/storage/private/update", "dm/read", - "group/create", "user/group/list", "group/info", + "group/create", "user/group/list", "group/info", "group/settings/update", "user/invite/accept", "user/invite/decline", "user/invite/revoke", "user/dm/list", "user/group/list", "user/storage/public/update", "user/storage/private/update", "group/storage/public/update", "group/storage/private/update", "group/storage/private/getentry", "dm/read", "group/read", "dm/message/send", "dm/message/delete", "dm/message/react/add", "dm/message/react/remove", "dm/message/pin/toggle", @@ -806,16 +806,25 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec break; case "group/create": { - Universal2String groupCreate = JsonSerializer.Deserialize( + Universal3String groupCreate = JsonSerializer.Deserialize( serializedBody.string2, - AppJsonSerializerContext.Default.Universal2String + AppJsonSerializerContext.Default.Universal3String ); - await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupCreate(id, groupCreate.string1, groupCreate.string2), password)); + await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupCreate(id, groupCreate.string1, groupCreate.string2, groupCreate.string3), password)); break; } case "group/info": await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInfo(serializedBody.string2), password)); break; + case "group/settings/update": + { + Universal3String req = JsonSerializer.Deserialize( + serializedBody.string2, + AppJsonSerializerContext.Default.Universal3String + ); + await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupSettingsUpdate(id, req.string1, req.string2, req.string3), password)); + break; + } case "group/invite": { Universal2String payload = JsonSerializer.Deserialize( diff --git a/LarpixServer/Room/Groups.cs b/LarpixServer/Room/Groups.cs index d7e355c..5967d88 100644 --- a/LarpixServer/Room/Groups.cs +++ b/LarpixServer/Room/Groups.cs @@ -53,7 +53,7 @@ public class Groups await Fs.WriteFile(groupPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(fileGroup, AppJsonSerializerContext.Default.Universal3String))); } - public static async Task GroupCreate(string id, string name, string body) + public static async Task GroupCreate(string id, string name, string body, string historyVisible = "false") { await Room.Requests.createLock.WaitAsync(); try @@ -84,6 +84,7 @@ public class Groups await Fs.WriteFile($"{groupPath}/keys/last", Encoding.UTF8.GetBytes("0")); await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("false")); await Fs.WriteFile($"{groupPath}/name", Encoding.UTF8.GetBytes(name)); + await Fs.WriteFile($"{groupPath}/history_visible", Encoding.UTF8.GetBytes(historyVisible == "true" ? "true" : "false")); // body is the key encrypted for the creator await Fs.WriteFile($"{groupPath}/keys/0/{id};{DOMAIN}", Encoding.UTF8.GetBytes(body)); @@ -149,6 +150,102 @@ public class Groups return $"{ROOMS_DIR}/groups/{domain}/{id}"; } + public static async Task GroupSettingsUpdate(string userId, string groupId, string name, string historyVisible) + { + string groupPath = GetGroupPath(groupId); + if (!Fs.Exists(groupPath)) return "error:group.not.found"; + + string memberPath = $"{groupPath}/members/{userId};{Utils.Utils.DOMAIN}"; + if (!Fs.Exists(memberPath)) return "error:not.a.member"; + + await Room.Requests.createLock.WaitAsync(); + try + { + if (!string.IsNullOrEmpty(name)) + { + if (string.IsNullOrWhiteSpace(name)) return "error:invalid.name"; + await Fs.WriteFile($"{groupPath}/name", Encoding.UTF8.GetBytes(name)); + } + + if (!string.IsNullOrEmpty(historyVisible)) + { + bool currentState = false; + if (Fs.Exists($"{groupPath}/history_visible")) + { + currentState = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible")) == "true"; + } + + bool newState = historyVisible == "true"; + + if (currentState != newState) + { + await Fs.WriteFile($"{groupPath}/history_visible", Encoding.UTF8.GetBytes(newState ? "true" : "false")); + + string timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); + + Message infoMsg = new(); + infoMsg.attachment = ""; + infoMsg.author = userId; + infoMsg.timestamp = timestamp; + infoMsg.type = "larp.info.history"; + infoMsg.content = newState ? "{blah(history.visible)}" : "{blah(history.hidden)}"; + infoMsg.key = ""; + + ulong lastMessageId = 0; + if (Fs.Exists($"{groupPath}/messages/last")) + { + lastMessageId = ulong.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/messages/last"))); + } + ulong newMessageId = lastMessageId + 1; + + infoMsg.pervious = lastMessageId.ToString(); + + await Fs.WriteFile($"{groupPath}/messages/{newMessageId}", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(infoMsg, AppJsonSerializerContext.Default.Message))); + await Fs.WriteFile($"{groupPath}/messages/last", Encoding.UTF8.GetBytes(newMessageId.ToString())); + + string[] members = Fs.ReadDirectory($"{groupPath}/members"); + foreach (string memberWD in members) + { + string memberStr = Path.GetFileName(memberWD); + bool isLocal = Account.Utils.IsUserLocal(memberStr, out string d); + string memberId = Account.Utils.GetValidIdOrZero(memberStr); + if (isLocal) + { + if (memberId == userId) + { + await UpdateUserGroup(memberId, groupId, "true", timestamp); + await Utils.Utils.SendWsMessage(memberId, $"group_message:{groupId}:read"); + } + else + { + _ = Task.Run(async () => + { + SemaphoreSlim userLock = Account.Utils.GetUserLock(memberId); + await userLock.WaitAsync(); + try + { + await UpdateUserGroup(memberId, groupId, "false", timestamp); + } + finally + { + userLock.Release(); + } + await Utils.Utils.SendWsMessage(memberId, $"group_message:{groupId}:unread"); + }); + } + } + } + } + } + } + finally + { + Room.Requests.createLock.Release(); + } + + return "success"; + } + public static async Task GroupInfo(string groupId) { string groupPath = GetGroupPath(groupId); @@ -181,16 +278,23 @@ public class Groups newRequired = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/keys/newrequired")); } - var info = new Universal5String + string historyVisible = "false"; + if (Fs.Exists($"{groupPath}/history_visible")) + { + historyVisible = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible")); + } + + var info = new Universal6String { string1 = groupId, string2 = name, string3 = memberCount, string4 = lastKeyIndex, - string5 = newRequired + string5 = newRequired, + string6 = historyVisible }; - return JsonSerializer.Serialize(info, AppJsonSerializerContext.Default.Universal5String); + return JsonSerializer.Serialize(info, AppJsonSerializerContext.Default.Universal6String); } public static async Task GroupInvite(string id, string groupId, string targetId) @@ -319,9 +423,16 @@ public class Groups // Add to members await Fs.WriteFile($"{groupPath}/members/{id};{DOMAIN}", Array.Empty()); - // Require new key - await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); - + // Require new key if history is not visible + bool historyVisible = false; + if (Fs.Exists($"{groupPath}/history_visible")) + { + historyVisible = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible")) == "true"; + } + if (!historyVisible) + { + await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); + } string timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); Message joinMsg = new(); @@ -520,6 +631,42 @@ public class Groups return "success:storage.entry.updated"; } + public static async Task GroupDelete(string groupId) + { + string[] parts = groupId.Split(';'); + string id = parts[0]; + string domain = parts.Length > 1 ? parts[1] : Utils.Utils.DOMAIN; + string groupPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}"; + + if (!Fs.Exists(groupPath)) return "error:group.not.found"; + + await Room.Requests.createLock.WaitAsync(); + try + { + string membersDir = $"{groupPath}/members"; + if (Fs.Exists(membersDir)) + { + string[] memberFiles = Fs.ReadDirectory(membersDir); + foreach (string memberFile in memberFiles) + { + string memberId = memberFile.Split(';')[0]; + string userGroupPath = $"{Utils.Utils.ACCOUNTS_DATA_DIR}/{memberId}/groups/{id};{domain}"; + if (Fs.Exists(userGroupPath)) + { + Fs.DeleteFile(userGroupPath); + } + } + } + Fs.DeleteDirectory(groupPath); + } + finally + { + Room.Requests.createLock.Release(); + } + + return "success"; + } + public static async Task GroupLeave(string userId, string groupId) { string[] parts = groupId.Split(';'); @@ -547,8 +694,16 @@ public class Groups Fs.DeleteFile(userGroupPath); } - // Require new key - await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); + // Require new key if history is not visible + bool historyVisible = false; + if (Fs.Exists($"{groupPath}/history_visible")) + { + historyVisible = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible")) == "true"; + } + if (!historyVisible) + { + await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); + } // Add info message Message msg = new(); @@ -575,6 +730,13 @@ public class Groups Room.Requests.createLock.Release(); } + // If members count == 0, delete group folder and we are done. + // Doing it outside the lock since GroupDelete takes the lock again. + if (Fs.ReadDirectory($"{groupPath}/members").Length == 0) + { + await GroupDelete(groupId); + } + return "success"; } } diff --git a/LarpixServer/Utils/AppJsonSerializerContext.cs b/LarpixServer/Utils/AppJsonSerializerContext.cs index 51e875a..606e290 100644 --- a/LarpixServer/Utils/AppJsonSerializerContext.cs +++ b/LarpixServer/Utils/AppJsonSerializerContext.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; [JsonSerializable(typeof(Universal3String))] [JsonSerializable(typeof(Universal4String))] [JsonSerializable(typeof(Universal5String))] +[JsonSerializable(typeof(Universal6String))] [JsonSerializable(typeof(ServerInfo))] [JsonSerializable(typeof(Message))] [JsonSerializable(typeof(Dictionary))] diff --git a/LarpixServer/Utils/Jsons.cs b/LarpixServer/Utils/Jsons.cs index 3c9e25e..8c6dcc5 100644 --- a/LarpixServer/Utils/Jsons.cs +++ b/LarpixServer/Utils/Jsons.cs @@ -34,6 +34,15 @@ public class Universal5String public string string5 { get; set; } } +public class Universal6String +{ + public string string1 { get; set; } + public string string2 { get; set; } + public string string3 { get; set; } + public string string4 { get; set; } + public string string5 { get; set; } + public string string6 { get; set; } +} public class KeyExchangePayload {