bug fixes
All checks were successful
Server Build / publish (push) Successful in 32s
Voice Build / publish (push) Successful in 25s

This commit is contained in:
olcxja 2026-08-17 11:59:26 +02:00
commit 5958bcdb17
5 changed files with 317 additions and 6 deletions

View file

@ -870,6 +870,33 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupLeave(id, payload.string1), password)); await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupLeave(id, payload.string1), password));
break; 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": case "group/members":
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupMembers(id, serializedBody.string2), password)); await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupMembers(id, serializedBody.string2), password));
break; break;
@ -1117,7 +1144,20 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
return; 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); byte[] fileBytes = await Utils.NonceDecryptBytes(id, password, bodyBytes, true);
if (fileBytes == null) if (fileBytes == null)
@ -1162,7 +1202,20 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
return; 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) if (fileBytes == null || fileBytes.Length == 0)
{ {
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:empty.body", password)); await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:empty.body", password));

View file

@ -89,6 +89,7 @@ public class Groups
// body is the key encrypted for the creator // body is the key encrypted for the creator
await Fs.WriteFile($"{groupPath}/keys/0/{id};{DOMAIN}", Encoding.UTF8.GetBytes(body)); 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(); Message startingMessage = new();
startingMessage.attachment = ""; startingMessage.attachment = "";
@ -171,6 +172,18 @@ public class Groups
if (!string.IsNullOrEmpty(historyVisible)) 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; bool currentState = false;
if (Fs.Exists($"{groupPath}/history_visible")) if (Fs.Exists($"{groupPath}/history_visible"))
{ {
@ -286,17 +299,24 @@ public class Groups
historyVisible = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible")); 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, string1 = groupId,
string2 = name, string2 = name,
string3 = memberCount, string3 = memberCount,
string4 = lastKeyIndex, string4 = lastKeyIndex,
string5 = newRequired, 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<string> GroupInvite(string id, string groupId, string targetId, string keysPayload = "") public static async Task<string> GroupInvite(string id, string groupId, string targetId, string keysPayload = "")
@ -857,6 +877,24 @@ public class Groups
// Notify remaining members via WebSocket // Notify remaining members via WebSocket
string[] currentMembers = Fs.ReadDirectory($"{groupPath}/members"); 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) foreach (string memberWD in currentMembers)
{ {
string memberStr = Path.GetFileName(memberWD); string memberStr = Path.GetFileName(memberWD);
@ -884,4 +922,159 @@ public class Groups
return "success"; return "success";
} }
public static async Task<string> 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<string> 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";
}
} }

View file

@ -39,6 +39,48 @@ public class Shared
return Fs.Exists(memberPath); return Fs.Exists(memberPath);
} }
public static async Task<string> 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<string> GetMessages(string id, string roomType, string roomId, string startOffset, string countStr) public static async Task<string> GetMessages(string id, string roomType, string roomId, string startOffset, string countStr)
{ {
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden"; if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
@ -276,7 +318,18 @@ public class Shared
AppJsonSerializerContext.Default.Message 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"; if (msg.type == "larp.redacted") return "error:message.already.deleted";
msg.type = "larp.redacted"; msg.type = "larp.redacted";

View file

@ -12,6 +12,7 @@ using System.Collections.Generic;
[JsonSerializable(typeof(Universal4String))] [JsonSerializable(typeof(Universal4String))]
[JsonSerializable(typeof(Universal5String))] [JsonSerializable(typeof(Universal5String))]
[JsonSerializable(typeof(Universal6String))] [JsonSerializable(typeof(Universal6String))]
[JsonSerializable(typeof(Universal7String))]
[JsonSerializable(typeof(ServerInfo))] [JsonSerializable(typeof(ServerInfo))]
[JsonSerializable(typeof(Message))] [JsonSerializable(typeof(Message))]
[JsonSerializable(typeof(Dictionary<string, string>))] [JsonSerializable(typeof(Dictionary<string, string>))]

View file

@ -44,6 +44,17 @@ public class Universal6String
public string string6 { get; set; } 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 class KeyExchangePayload
{ {
public string pubX25519 { get; set; } public string pubX25519 { get; set; }