From 5958bcdb17e90ec6a29c28d07855eca516a75c16 Mon Sep 17 00:00:00 2001 From: olcxja Date: Mon, 17 Aug 2026 11:59:26 +0200 Subject: [PATCH] bug fixes --- LarpixServer/Account/Requests.cs | 57 ++++- LarpixServer/Room/Groups.cs | 199 +++++++++++++++++- LarpixServer/Room/Shared.cs | 55 ++++- .../Utils/AppJsonSerializerContext.cs | 1 + LarpixServer/Utils/Jsons.cs | 11 + 5 files changed, 317 insertions(+), 6 deletions(-) diff --git a/LarpixServer/Account/Requests.cs b/LarpixServer/Account/Requests.cs index 7080d5b..29e2e1b 100644 --- a/LarpixServer/Account/Requests.cs +++ b/LarpixServer/Account/Requests.cs @@ -870,6 +870,33 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupLeave(id, payload.string1), password)); break; } + case "group/kick": + { + Universal2String payload = JsonSerializer.Deserialize( + serializedBody.string2, + AppJsonSerializerContext.Default.Universal2String + ); + await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupKickMember(id, payload.string1, payload.string2), password)); + break; + } + case "group/transferowner": + { + Universal2String payload = JsonSerializer.Deserialize( + serializedBody.string2, + AppJsonSerializerContext.Default.Universal2String + ); + await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupTransferOwner(id, payload.string1, payload.string2), password)); + break; + } + case "room/role": + { + Universal3String payload = JsonSerializer.Deserialize( + serializedBody.string2, + AppJsonSerializerContext.Default.Universal3String + ); + await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Shared.GetUserRole(id, payload.string1, payload.string2, payload.string3), password)); + break; + } case "group/members": await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupMembers(id, serializedBody.string2), password)); break; @@ -1117,7 +1144,20 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec return; } - byte[] bodyBytes = await LarpixServer.Utils.Utils.LoadBodyBytes(context.Request.Body, LarpixServer.Utils.Utils.PUBLIC_STORAGE_LIMIT); + byte[] bodyBytes = null; + try + { + bodyBytes = await LarpixServer.Utils.Utils.LoadBodyBytes(context.Request.Body, LarpixServer.Utils.Utils.PUBLIC_STORAGE_LIMIT); + } + catch (Exception ex) + { + if (ex.Message.StartsWith("error:")) + { + await context.Response.WriteAsync(Encryption.Encryption.EncryptString(ex.Message, password)); + return; + } + throw; + } byte[] fileBytes = await Utils.NonceDecryptBytes(id, password, bodyBytes, true); if (fileBytes == null) @@ -1162,7 +1202,20 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec return; } - byte[] fileBytes = await LoadBodyBytes(context.Request.Body, ATTACHMENT_SIZE_LIMIT); // attachment size limit + byte[] fileBytes = null; + try + { + fileBytes = await LoadBodyBytes(context.Request.Body, ATTACHMENT_SIZE_LIMIT); // attachment size limit + } + catch (Exception ex) + { + if (ex.Message.StartsWith("error:")) + { + await context.Response.WriteAsync(Encryption.Encryption.EncryptString(ex.Message, password)); + return; + } + throw; + } if (fileBytes == null || fileBytes.Length == 0) { await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:empty.body", password)); diff --git a/LarpixServer/Room/Groups.cs b/LarpixServer/Room/Groups.cs index bad84bc..d200b5d 100644 --- a/LarpixServer/Room/Groups.cs +++ b/LarpixServer/Room/Groups.cs @@ -89,6 +89,7 @@ public class Groups // body is the key encrypted for the creator await Fs.WriteFile($"{groupPath}/keys/0/{id};{DOMAIN}", Encoding.UTF8.GetBytes(body)); + await Fs.WriteFile($"{groupPath}/owner", Encoding.UTF8.GetBytes($"{id};{DOMAIN}")); Message startingMessage = new(); startingMessage.attachment = ""; @@ -171,6 +172,18 @@ public class Groups if (!string.IsNullOrEmpty(historyVisible)) { + string ownerPath = $"{groupPath}/owner"; + string currentOwner = ""; + if (Fs.Exists(ownerPath)) + { + currentOwner = Encoding.UTF8.GetString(await Fs.ReadFile(ownerPath)); + } + + if (currentOwner != $"{userId};{Utils.Utils.DOMAIN}") + { + return "error:forbidden"; + } + bool currentState = false; if (Fs.Exists($"{groupPath}/history_visible")) { @@ -286,17 +299,24 @@ public class Groups historyVisible = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible")); } - var info = new Universal6String + string owner = ""; + if (Fs.Exists($"{groupPath}/owner")) + { + owner = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/owner")); + } + + var info = new Universal7String { string1 = groupId, string2 = name, string3 = memberCount, string4 = lastKeyIndex, string5 = newRequired, - string6 = historyVisible + string6 = historyVisible, + string7 = owner }; - return JsonSerializer.Serialize(info, AppJsonSerializerContext.Default.Universal6String); + return JsonSerializer.Serialize(info, AppJsonSerializerContext.Default.Universal7String); } public static async Task GroupInvite(string id, string groupId, string targetId, string keysPayload = "") @@ -857,6 +877,24 @@ public class Groups // Notify remaining members via WebSocket string[] currentMembers = Fs.ReadDirectory($"{groupPath}/members"); + + // Check if leaving user was owner, transfer ownership if possible + if (Fs.Exists($"{groupPath}/owner")) + { + string currentOwner = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/owner")); + if (currentOwner == $"{userId};{Utils.Utils.DOMAIN}") + { + if (currentMembers.Length > 0) + { + await Fs.WriteFile($"{groupPath}/owner", Encoding.UTF8.GetBytes(currentMembers[0])); + } + else + { + Fs.DeleteFile($"{groupPath}/owner"); + } + } + } + foreach (string memberWD in currentMembers) { string memberStr = Path.GetFileName(memberWD); @@ -884,4 +922,159 @@ public class Groups return "success"; } + + public static async Task GroupKickMember(string id, string groupId, string targetId) + { + string groupPath = GetGroupPath(groupId); + if (!Fs.Exists(groupPath)) return "error:group.not.found"; + + string ownerPath = $"{groupPath}/owner"; + if (!Fs.Exists(ownerPath)) return "error:forbidden"; + + string currentOwner = Encoding.UTF8.GetString(await Fs.ReadFile(ownerPath)); + if (currentOwner != $"{id};{DOMAIN}") return "error:forbidden"; + + string tId = Account.Utils.GetValidIdOrZero(targetId); + if (tId == "0") return "error:invalid.user"; + + bool isLocal = Account.Utils.IsUserLocal(targetId, out string tDomain); + string memberPath = $"{groupPath}/members/{tId};{tDomain}"; + + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); + await roomLock.WaitAsync(); + try + { + if (!Fs.Exists(memberPath)) return "error:not.a.member"; + if (tId == id) return "error:cannot.kick.self"; + + Fs.DeleteFile(memberPath); + + string userGroupPath = $"{Utils.Utils.ACCOUNTS_DATA_DIR}/{tId}/groups/{groupId}"; + if (Fs.Exists(userGroupPath)) + { + Fs.DeleteFile(userGroupPath); + } + + await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); + + string targetName = await Account.Utils.NameFromId(tId); + string targetNameWithHost = isLocal ? targetName : $"{targetName}:{tDomain}"; + + Message msg = new(); + msg.attachment = ""; + msg.author = id; + msg.timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); + msg.type = "larp.info.kick"; + msg.content = $"{tId};{tDomain}:{targetNameWithHost}"; + msg.key = ""; + msg.pervious = ""; + msg.responded = ""; + + ulong msgId = 0; + if (Fs.Exists($"{groupPath}/messages/last")) + { + msgId = ulong.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/messages/last"))); + msgId++; + } + await Fs.WriteFile($"{groupPath}/messages/last", Encoding.UTF8.GetBytes(msgId.ToString())); + await Fs.WriteFile($"{groupPath}/messages/{msgId}", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg, AppJsonSerializerContext.Default.Message))); + + string[] currentMembers = Fs.ReadDirectory($"{groupPath}/members"); + foreach (string memberWD in currentMembers) + { + string memberStr = Path.GetFileName(memberWD); + string memberIdStr = Account.Utils.GetValidIdOrZero(memberStr); + if (Account.Utils.IsUserLocal(memberStr, out string d)) + { + _ = Task.Run(async () => + { + await Utils.Utils.SendWsMessage(memberIdStr, $"group_message:{groupId}:unread"); + }); + } + } + + if (isLocal) + { + _ = Task.Run(async () => + { + await Utils.Utils.SendWsMessage(tId, $"group_removed:{groupId}"); + }); + } + } + finally + { + roomLock.Release(); + } + + return "success"; + } + + public static async Task GroupTransferOwner(string id, string groupId, string targetId) + { + string groupPath = GetGroupPath(groupId); + if (!Fs.Exists(groupPath)) return "error:group.not.found"; + + string ownerPath = $"{groupPath}/owner"; + if (!Fs.Exists(ownerPath)) return "error:forbidden"; + + string currentOwner = Encoding.UTF8.GetString(await Fs.ReadFile(ownerPath)); + if (currentOwner != $"{id};{DOMAIN}") return "error:forbidden"; + + string tId = Account.Utils.GetValidIdOrZero(targetId); + if (tId == "0") return "error:invalid.user"; + + bool isLocal = Account.Utils.IsUserLocal(targetId, out string tDomain); + string memberPath = $"{groupPath}/members/{tId};{tDomain}"; + + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); + await roomLock.WaitAsync(); + try + { + if (!Fs.Exists(memberPath)) return "error:not.a.member"; + + await Fs.WriteFile(ownerPath, Encoding.UTF8.GetBytes($"{tId};{tDomain}")); + + string targetName = await Account.Utils.NameFromId(tId); + string targetNameWithHost = isLocal ? targetName : $"{targetName}:{tDomain}"; + + Message msg = new(); + msg.attachment = ""; + msg.author = id; + msg.timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); + msg.type = "larp.info.transfer_owner"; + msg.content = $"{tId};{tDomain}:{targetNameWithHost}"; + msg.key = ""; + msg.pervious = ""; + msg.responded = ""; + + ulong msgId = 0; + if (Fs.Exists($"{groupPath}/messages/last")) + { + msgId = ulong.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/messages/last"))); + msgId++; + } + await Fs.WriteFile($"{groupPath}/messages/last", Encoding.UTF8.GetBytes(msgId.ToString())); + await Fs.WriteFile($"{groupPath}/messages/{msgId}", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg, AppJsonSerializerContext.Default.Message))); + + string[] currentMembers = Fs.ReadDirectory($"{groupPath}/members"); + foreach (string memberWD in currentMembers) + { + string memberStr = Path.GetFileName(memberWD); + string memberIdStr = Account.Utils.GetValidIdOrZero(memberStr); + if (Account.Utils.IsUserLocal(memberStr, out string d)) + { + _ = Task.Run(async () => + { + await Utils.Utils.SendWsMessage(memberIdStr, $"group_message:{groupId}:unread"); + }); + } + } + } + finally + { + roomLock.Release(); + } + + return "success"; + } } diff --git a/LarpixServer/Room/Shared.cs b/LarpixServer/Room/Shared.cs index 339af72..74c9442 100644 --- a/LarpixServer/Room/Shared.cs +++ b/LarpixServer/Room/Shared.cs @@ -39,6 +39,48 @@ public class Shared return Fs.Exists(memberPath); } + public static async Task GetUserRole(string id, string roomType, string roomId, string targetUserId) + { + if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden"; + + string targetMemberPath = $"{GetRoomPath(roomType, roomId)}/members/{targetUserId}"; + if (!Fs.Exists(targetMemberPath)) return "error:not.a.member"; + + if (roomType == "dms") + { + return "member"; + } + else if (roomType == "groups") + { + string ownerPath = $"{Groups.GetGroupPath(roomId)}/owner"; + if (Fs.Exists(ownerPath)) + { + string owner = Encoding.UTF8.GetString(await Fs.ReadFile(ownerPath)); + if (owner == targetUserId) + { + return "group owner"; + } + } + return "member"; + } + else if (roomType == "spaces") + { + // Placeholder for spaces where custom roles will be implemented + string ownerPath = $"{ROOMS_DIR}/{roomType}/{DOMAIN}/{roomId}/owner"; + if (Fs.Exists(ownerPath)) + { + string owner = Encoding.UTF8.GetString(await Fs.ReadFile(ownerPath)); + if (owner == targetUserId) + { + return "Owner"; + } + } + return "member"; + } + + return "member"; + } + public static async Task GetMessages(string id, string roomType, string roomId, string startOffset, string countStr) { if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden"; @@ -276,7 +318,18 @@ public class Shared AppJsonSerializerContext.Default.Message ); - if (msg.author != id) return "error:forbidden"; + if (msg.author != id) + { + if (roomType == "groups") + { + string role = await GetUserRole(id, roomType, roomId, id); + if (role != "group owner") return "error:forbidden"; + } + else + { + return "error:forbidden"; + } + } if (msg.type == "larp.redacted") return "error:message.already.deleted"; msg.type = "larp.redacted"; diff --git a/LarpixServer/Utils/AppJsonSerializerContext.cs b/LarpixServer/Utils/AppJsonSerializerContext.cs index 606e290..4b57a72 100644 --- a/LarpixServer/Utils/AppJsonSerializerContext.cs +++ b/LarpixServer/Utils/AppJsonSerializerContext.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; [JsonSerializable(typeof(Universal4String))] [JsonSerializable(typeof(Universal5String))] [JsonSerializable(typeof(Universal6String))] +[JsonSerializable(typeof(Universal7String))] [JsonSerializable(typeof(ServerInfo))] [JsonSerializable(typeof(Message))] [JsonSerializable(typeof(Dictionary))] diff --git a/LarpixServer/Utils/Jsons.cs b/LarpixServer/Utils/Jsons.cs index 8c6dcc5..ab2656c 100644 --- a/LarpixServer/Utils/Jsons.cs +++ b/LarpixServer/Utils/Jsons.cs @@ -44,6 +44,17 @@ public class Universal6String public string string6 { get; set; } } +public class Universal7String +{ + 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 string string7 { get; set; } +} + public class KeyExchangePayload { public string pubX25519 { get; set; }