731 lines
28 KiB
C#
731 lines
28 KiB
C#
using System.Numerics;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using LarpixServer.Filesystem;
|
|
using LarpixServer.Utils.Jsons;
|
|
using static LarpixServer.Utils.Utils;
|
|
|
|
namespace LarpixServer.Room;
|
|
|
|
public class Shared
|
|
{
|
|
public static string GetRoomPath(string roomType, string roomId)
|
|
{
|
|
if (roomType == "groups")
|
|
{
|
|
return Groups.GetGroupPath(roomId);
|
|
}
|
|
else if (roomType == "dms")
|
|
{
|
|
return $"{ROOMS_DIR}/dms/{DOMAIN}/{roomId}";
|
|
}
|
|
return $"{ROOMS_DIR}/{roomType}/{DOMAIN}/{roomId}";
|
|
}
|
|
|
|
private static bool IsSafeId(string msgId)
|
|
{
|
|
if (string.IsNullOrEmpty(msgId)) return false;
|
|
if (msgId.Contains("..") || msgId.Contains("/") || msgId.Contains("\\"))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static async Task<bool> IsMemberOfRoom(string id, string roomType, string roomId)
|
|
{
|
|
if (!IsSafeId(roomId)) return false;
|
|
string memberPath = $"{GetRoomPath(roomType, roomId)}/members/{id};{DOMAIN}";
|
|
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)
|
|
{
|
|
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
|
|
|
string msgDir = $"{GetRoomPath(roomType, roomId)}/messages";
|
|
if (!Fs.Exists(msgDir)) return "{}";
|
|
|
|
long last = 0;
|
|
if (Fs.Exists($"{msgDir}/last"))
|
|
{
|
|
string lastStr = Encoding.UTF8.GetString(await Fs.ReadFile($"{msgDir}/last"));
|
|
long.TryParse(lastStr, out last);
|
|
}
|
|
|
|
long start = last;
|
|
if (!string.IsNullOrEmpty(startOffset) && long.TryParse(startOffset, out long s))
|
|
{
|
|
start = s;
|
|
}
|
|
|
|
int count = 50;
|
|
if (!string.IsNullOrEmpty(countStr) && int.TryParse(countStr, out int c))
|
|
{
|
|
count = Math.Min(c, 100);
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder("{");
|
|
int fetched = 0;
|
|
for (long i = start; i >= 0 && fetched < count; i--)
|
|
{
|
|
string p = $"{msgDir}/{i}";
|
|
if (Fs.Exists(p))
|
|
{
|
|
if (fetched > 0) sb.Append(',');
|
|
sb.Append($"\"{i}\":");
|
|
|
|
string msgJson = Encoding.UTF8.GetString(await Fs.ReadFile(p));
|
|
Message msgObj = JsonSerializer.Deserialize(msgJson, AppJsonSerializerContext.Default.Message);
|
|
|
|
string reactDir = $"{GetRoomPath(roomType, roomId)}/reactions/{i}";
|
|
if (Fs.Exists(reactDir))
|
|
{
|
|
StringBuilder rxSb = new StringBuilder("[");
|
|
string[] reactFiles = Fs.ReadDirectory(reactDir);
|
|
int rxCount = 0;
|
|
foreach(string rf in reactFiles)
|
|
{
|
|
string rId = System.IO.Path.GetFileName(rf);
|
|
if (rId == "last") continue;
|
|
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{reactDir}/{rId}"));
|
|
int nLineIdx = content.IndexOf('\n');
|
|
if (nLineIdx != -1)
|
|
{
|
|
string u = content.Substring(0, nLineIdx).Trim();
|
|
string r = content.Substring(nLineIdx + 1).Trim();
|
|
if (rxCount > 0) rxSb.Append(',');
|
|
rxSb.Append($"{{\"id\":\"{rId}\",\"u\":\"{u}\",\"r\":\"{r}\"}}");
|
|
rxCount++;
|
|
}
|
|
}
|
|
rxSb.Append("]");
|
|
msgObj.reactions = rxCount > 0 ? rxSb.ToString() : "";
|
|
}
|
|
else
|
|
{
|
|
msgObj.reactions = "";
|
|
}
|
|
|
|
if (Fs.Exists($"{GetRoomPath(roomType, roomId)}/pinned/{i}"))
|
|
{
|
|
msgObj.pinned = "true";
|
|
}
|
|
|
|
sb.Append(JsonSerializer.Serialize(msgObj, AppJsonSerializerContext.Default.Message));
|
|
fetched++;
|
|
}
|
|
}
|
|
sb.Append("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
public static async Task<string> MessageSend(string id, string roomType, string roomId, string content, string attachment, string responded, string keyIndex = "0")
|
|
{
|
|
string msgDir = $"{GetRoomPath(roomType, roomId)}/messages";
|
|
|
|
long nextId = 0;
|
|
Message msg = new Message();
|
|
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId);
|
|
await roomLock.WaitAsync();
|
|
try
|
|
{
|
|
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
|
|
|
if (roomType == "groups")
|
|
{
|
|
string newRequiredPath = $"{Groups.GetGroupPath(roomId)}/keys/newrequired";
|
|
if (Fs.Exists(newRequiredPath))
|
|
{
|
|
string isRequired = Encoding.UTF8.GetString(await Fs.ReadFile(newRequiredPath));
|
|
if (isRequired == "true")
|
|
{
|
|
return "error:key.rotation.required";
|
|
}
|
|
}
|
|
|
|
// Check if keyIndex matches last key
|
|
string lastKeyPath = $"{Groups.GetGroupPath(roomId)}/keys/last";
|
|
if (Fs.Exists(lastKeyPath))
|
|
{
|
|
string lastKey = Encoding.UTF8.GetString(await Fs.ReadFile(lastKeyPath));
|
|
if (keyIndex != lastKey && keyIndex != "") // empty key means larp.info messages
|
|
{
|
|
return "error:invalid.key.index";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (content != null && content.Length > LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE)
|
|
{
|
|
string sizeStr = LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE >= 1024 ? $"{LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE / 1024.0:0.##} KB" : $"{LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE} B";
|
|
return $"error:message.too.long:{sizeStr}";
|
|
}
|
|
|
|
long last = 0;
|
|
if (Fs.Exists($"{msgDir}/last"))
|
|
{
|
|
last = long.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{msgDir}/last")));
|
|
}
|
|
nextId = last + 1;
|
|
|
|
msg.author = id;
|
|
msg.timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
|
|
msg.type = "larp.text";
|
|
msg.content = content;
|
|
msg.attachment = attachment;
|
|
msg.key = keyIndex;
|
|
msg.pervious = "";
|
|
msg.responded = responded ?? "";
|
|
msg.reactions = "";
|
|
|
|
if (!string.IsNullOrEmpty(msg.attachment) && msg.attachment != "[]")
|
|
{
|
|
try
|
|
{
|
|
var attachmentsNode = System.Text.Json.Nodes.JsonNode.Parse(msg.attachment);
|
|
if (attachmentsNode is System.Text.Json.Nodes.JsonArray arr)
|
|
{
|
|
string attDir = $"{GetRoomPath(roomType, roomId)}/attachments/{nextId}";
|
|
if (!Fs.Exists(attDir)) Fs.CreateDirectory(attDir);
|
|
|
|
for (int i = 0; i < arr.Count; i++)
|
|
{
|
|
var attObj = arr[i] as System.Text.Json.Nodes.JsonObject;
|
|
if (attObj != null && attObj.ContainsKey("staging_id"))
|
|
{
|
|
string stagingId = attObj["staging_id"].ToString();
|
|
string stagingPath = $"{ROOMS_DIR}/dms_staging/{stagingId}";
|
|
if (Fs.Exists(stagingPath))
|
|
{
|
|
string attId = attObj["id"].ToString();
|
|
string destPath = $"{attDir}/{attId}";
|
|
Fs.MoveFile(stagingPath, destPath);
|
|
}
|
|
attObj.Remove("staging_id");
|
|
}
|
|
}
|
|
msg.attachment = attachmentsNode.ToJsonString();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error processing attachments: {ex}");
|
|
}
|
|
}
|
|
|
|
await Fs.WriteFile($"{msgDir}/{nextId}", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg, AppJsonSerializerContext.Default.Message)));
|
|
await Fs.WriteFile($"{msgDir}/last", Encoding.UTF8.GetBytes(nextId.ToString()));
|
|
}
|
|
finally
|
|
{
|
|
roomLock.Release();
|
|
}
|
|
|
|
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/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)
|
|
{
|
|
if (roomType == "dms") await Account.Utils.UpdateUserDm(memberId, roomId, "true", msg.timestamp);
|
|
if (roomType == "groups") await Room.Groups.UpdateUserGroup(memberId, roomId, "true", msg.timestamp);
|
|
|
|
await Utils.Utils.SendWsMessage(memberId, $"{GetRoomEventPrefix(roomType)}_message:{roomId}:read");
|
|
}
|
|
else
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
SemaphoreSlim userLock = Account.Utils.GetUserLock(memberId);
|
|
await userLock.WaitAsync();
|
|
try
|
|
{
|
|
if (roomType == "dms") await Account.Utils.UpdateUserDm(memberId, roomId, "false", msg.timestamp);
|
|
if (roomType == "groups") await Room.Groups.UpdateUserGroup(memberId, roomId, "false", msg.timestamp);
|
|
}
|
|
finally
|
|
{
|
|
userLock.Release();
|
|
}
|
|
await Utils.Utils.SendWsMessage(memberId, $"{GetRoomEventPrefix(roomType)}_message:{roomId}:unread");
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return $"success:{nextId}";
|
|
}
|
|
|
|
public static async Task<string> MessageDelete(string id, string roomType, string roomId, string msgId)
|
|
{
|
|
if (!IsSafeId(msgId)) return "error:invalid.msg.id";
|
|
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
|
|
|
string msgPath = $"{GetRoomPath(roomType, roomId)}/messages/{msgId}";
|
|
if (!Fs.Exists(msgPath)) return "error:message.not.found";
|
|
|
|
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId);
|
|
await roomLock.WaitAsync();
|
|
try
|
|
{
|
|
Message msg = JsonSerializer.Deserialize(
|
|
Encoding.UTF8.GetString(await Fs.ReadFile(msgPath)),
|
|
AppJsonSerializerContext.Default.Message
|
|
);
|
|
|
|
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";
|
|
msg.reactions = "";
|
|
msg.content = "{blah(larp.redacted)}";
|
|
msg.attachment = "";
|
|
msg.key = "";
|
|
|
|
await Fs.WriteFile(msgPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg, AppJsonSerializerContext.Default.Message)));
|
|
|
|
string reactDir = $"{GetRoomPath(roomType, roomId)}/reactions/{msgId}";
|
|
if (Fs.Exists(reactDir))
|
|
{
|
|
Fs.DeleteDirectory(reactDir);
|
|
}
|
|
|
|
string attDir = $"{GetRoomPath(roomType, roomId)}/attachments/{msgId}";
|
|
if (Fs.Exists(attDir))
|
|
{
|
|
Fs.DeleteDirectory(attDir);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
roomLock.Release();
|
|
}
|
|
|
|
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");
|
|
foreach(string memberWD in members)
|
|
{
|
|
bool isLocal = Account.Utils.IsUserLocal(memberWD, out string domain);
|
|
string memberId = Account.Utils.GetValidIdOrZero(memberWD);
|
|
if (isLocal)
|
|
{
|
|
await Utils.Utils.SendWsMessage(memberId, $"{GetRoomEventPrefix(roomType)}_message:{roomId}:update");
|
|
}
|
|
}
|
|
return "success:message.deleted";
|
|
}
|
|
|
|
public static async Task<string> MessageReactAdd(string id, string roomType, string roomId, string msgId, string reaction)
|
|
{
|
|
if (!IsSafeId(msgId)) return "error:invalid.msg.id";
|
|
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
|
|
|
if (reaction != null && reaction.Length > LarpixServer.Utils.Utils.MAX_REACTION_SIZE)
|
|
{
|
|
string sizeStr = LarpixServer.Utils.Utils.MAX_REACTION_SIZE >= 1024 ? $"{LarpixServer.Utils.Utils.MAX_REACTION_SIZE / 1024.0:0.##} KB" : $"{LarpixServer.Utils.Utils.MAX_REACTION_SIZE} B";
|
|
return $"error:reaction.too.long:{sizeStr}";
|
|
}
|
|
|
|
string msgPath = $"{GetRoomPath(roomType, roomId)}/messages/{msgId}";
|
|
if (!Fs.Exists(msgPath)) return "error:message.not.found";
|
|
|
|
Message msg = JsonSerializer.Deserialize(
|
|
Encoding.UTF8.GetString(await Fs.ReadFile(msgPath)),
|
|
AppJsonSerializerContext.Default.Message
|
|
);
|
|
|
|
if (msg.type == "larp.redacted") return "error:message.already.deleted";
|
|
|
|
string reactDir = $"{GetRoomPath(roomType, roomId)}/reactions/{msgId}";
|
|
if (!Fs.Exists(reactDir)) Fs.CreateDirectory(reactDir);
|
|
|
|
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId);
|
|
await roomLock.WaitAsync();
|
|
try
|
|
{
|
|
long last = 0;
|
|
if (Fs.Exists($"{reactDir}/last"))
|
|
{
|
|
last = long.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{reactDir}/last")));
|
|
}
|
|
long nextId = last + 1;
|
|
|
|
await Fs.WriteFile($"{reactDir}/{nextId}", Encoding.UTF8.GetBytes($"{id}\n{reaction}"));
|
|
await Fs.WriteFile($"{reactDir}/last", Encoding.UTF8.GetBytes(nextId.ToString()));
|
|
}
|
|
finally
|
|
{
|
|
roomLock.Release();
|
|
}
|
|
|
|
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");
|
|
foreach(string memberWD in members)
|
|
{
|
|
bool isLocal = Account.Utils.IsUserLocal(memberWD, out string domain);
|
|
string memberId = Account.Utils.GetValidIdOrZero(memberWD);
|
|
if (isLocal)
|
|
{
|
|
await Utils.Utils.SendWsMessage(memberId, $"{GetRoomEventPrefix(roomType)}_message:{roomId}:update");
|
|
}
|
|
}
|
|
return "success:message.reacted";
|
|
}
|
|
|
|
public static async Task<string> MessageReactRemove(string id, string roomType, string roomId, string msgId, string reactionId)
|
|
{
|
|
if (!IsSafeId(msgId)) return "error:invalid.msg.id";
|
|
if (!IsSafeId(reactionId)) return "error:invalid.reaction.id";
|
|
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
|
|
|
string reactPath = $"{GetRoomPath(roomType, roomId)}/reactions/{msgId}/{reactionId}";
|
|
if (!Fs.Exists(reactPath)) return "error:reaction.not.found";
|
|
|
|
string content = Encoding.UTF8.GetString(await Fs.ReadFile(reactPath));
|
|
int nLineIdx = content.IndexOf('\n');
|
|
if (nLineIdx != -1)
|
|
{
|
|
string u = content.Substring(0, nLineIdx).Trim();
|
|
if (u != id) return "error:forbidden";
|
|
}
|
|
|
|
Fs.DeleteFile(reactPath);
|
|
|
|
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");
|
|
foreach(string memberWD in members)
|
|
{
|
|
bool isLocal = Account.Utils.IsUserLocal(memberWD, out string domain);
|
|
string memberId = Account.Utils.GetValidIdOrZero(memberWD);
|
|
if (isLocal)
|
|
{
|
|
await Utils.Utils.SendWsMessage(memberId, $"{GetRoomEventPrefix(roomType)}_message:{roomId}:update");
|
|
}
|
|
}
|
|
return "success:message.reaction.removed";
|
|
}
|
|
|
|
public static async Task<string> MessagePinToggle(string id, string roomType, string roomId, string msgId)
|
|
{
|
|
if (!IsSafeId(msgId)) return "error:invalid.msg.id";
|
|
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
|
|
|
string msgPath = $"{GetRoomPath(roomType, roomId)}/messages/{msgId}";
|
|
if (!Fs.Exists(msgPath)) return "error:message.not.found";
|
|
|
|
string pinnedDir = $"{GetRoomPath(roomType, roomId)}/pinned";
|
|
if (!System.IO.Directory.Exists(pinnedDir))
|
|
{
|
|
System.IO.Directory.CreateDirectory(pinnedDir);
|
|
}
|
|
|
|
string pinnedFilePath = $"{pinnedDir}/{msgId}";
|
|
bool isNowPinned = false;
|
|
|
|
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId);
|
|
await roomLock.WaitAsync();
|
|
try
|
|
{
|
|
if (Fs.Exists(pinnedFilePath))
|
|
{
|
|
Fs.DeleteFile(pinnedFilePath);
|
|
isNowPinned = false;
|
|
}
|
|
else
|
|
{
|
|
await Fs.WriteFile(pinnedFilePath, new byte[0]);
|
|
isNowPinned = true;
|
|
}
|
|
|
|
long nextId = 0;
|
|
if (Fs.Exists($"{GetRoomPath(roomType, roomId)}/messages/last"))
|
|
{
|
|
string lastStr = Encoding.UTF8.GetString(await Fs.ReadFile($"{GetRoomPath(roomType, roomId)}/messages/last"));
|
|
if (long.TryParse(lastStr, out long last))
|
|
{
|
|
nextId = last + 1;
|
|
}
|
|
}
|
|
|
|
Message infoMessage = new();
|
|
infoMessage.attachment = "";
|
|
infoMessage.author = id;
|
|
infoMessage.timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
|
|
infoMessage.type = isNowPinned ? "larp.info.pin" : "larp.info.unpin";
|
|
infoMessage.content = isNowPinned ? "{blah(message.pinned.by)}" : "{blah(message.unpinned.by)}";
|
|
infoMessage.key = "";
|
|
infoMessage.pervious = msgId;
|
|
|
|
string mPath = $"{GetRoomPath(roomType, roomId)}/messages/{nextId}";
|
|
await Fs.WriteFile(mPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(infoMessage, AppJsonSerializerContext.Default.Message)));
|
|
await Fs.WriteFile($"{GetRoomPath(roomType, roomId)}/messages/last", Encoding.UTF8.GetBytes(nextId.ToString()));
|
|
}
|
|
finally
|
|
{
|
|
roomLock.Release();
|
|
}
|
|
|
|
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");
|
|
foreach(string memberWD in members)
|
|
{
|
|
bool isLocal = Account.Utils.IsUserLocal(memberWD, out string domain);
|
|
string memberId = Account.Utils.GetValidIdOrZero(memberWD);
|
|
if (isLocal)
|
|
{
|
|
await Utils.Utils.SendWsMessage(memberId, $"{GetRoomEventPrefix(roomType)}_message:{roomId}:update");
|
|
}
|
|
}
|
|
return "success:message.pin.toggled";
|
|
}
|
|
|
|
public static async Task<string> MessageGetPinned(string id, string roomType, string roomId)
|
|
{
|
|
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
|
|
|
string pinnedDir = $"{GetRoomPath(roomType, roomId)}/pinned";
|
|
if (!System.IO.Directory.Exists(pinnedDir)) return "{}";
|
|
|
|
string[] pinnedFiles = Fs.ReadDirectory(pinnedDir);
|
|
StringBuilder sb = new StringBuilder("{");
|
|
int fetched = 0;
|
|
|
|
foreach (string pFile in pinnedFiles)
|
|
{
|
|
string msgId = System.IO.Path.GetFileName(pFile);
|
|
string msgPath = $"{GetRoomPath(roomType, roomId)}/messages/{msgId}";
|
|
|
|
if (Fs.Exists(msgPath))
|
|
{
|
|
if (fetched > 0) sb.Append(',');
|
|
sb.Append($"\"{msgId}\":");
|
|
|
|
string msgJson = Encoding.UTF8.GetString(await Fs.ReadFile(msgPath));
|
|
Message msgObj = JsonSerializer.Deserialize(msgJson, AppJsonSerializerContext.Default.Message);
|
|
|
|
string reactDir = $"{GetRoomPath(roomType, roomId)}/reactions/{msgId}";
|
|
if (Fs.Exists(reactDir))
|
|
{
|
|
StringBuilder rxSb = new StringBuilder("[");
|
|
string[] reactFiles = Fs.ReadDirectory(reactDir);
|
|
int rxCount = 0;
|
|
foreach(string rf in reactFiles)
|
|
{
|
|
string rId = System.IO.Path.GetFileName(rf);
|
|
if (rId == "last") continue;
|
|
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{reactDir}/{rId}"));
|
|
int nLineIdx = content.IndexOf('\n');
|
|
if (nLineIdx != -1)
|
|
{
|
|
string u = content.Substring(0, nLineIdx).Trim();
|
|
string r = content.Substring(nLineIdx + 1).Trim();
|
|
if (rxCount > 0) rxSb.Append(',');
|
|
rxSb.Append($"{{\"id\":\"{rId}\",\"u\":\"{u}\",\"r\":\"{r}\"}}");
|
|
rxCount++;
|
|
}
|
|
}
|
|
rxSb.Append("]");
|
|
msgObj.reactions = rxCount > 0 ? rxSb.ToString() : "";
|
|
}
|
|
else
|
|
{
|
|
msgObj.reactions = "";
|
|
}
|
|
|
|
msgObj.pinned = "true";
|
|
|
|
sb.Append(JsonSerializer.Serialize(msgObj, AppJsonSerializerContext.Default.Message));
|
|
fetched++;
|
|
}
|
|
}
|
|
sb.Append("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string GetRoomEventPrefix(string roomType)
|
|
{
|
|
if (roomType == "dms") return "dm";
|
|
if (roomType == "groups") return "group";
|
|
return "room";
|
|
}
|
|
|
|
public static async Task<string> 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<string>());
|
|
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<string> 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<string> 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";
|
|
}
|
|
}
|