653 lines
No EOL
25 KiB
C#
653 lines
No EOL
25 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 Requests
|
|
{
|
|
public static SemaphoreSlim createLock = new SemaphoreSlim(1, 1);
|
|
private const int MAX_DM_KEY_SIZE = 8192;
|
|
|
|
private static bool IsSafeDmId(string dmId)
|
|
{
|
|
if (string.IsNullOrEmpty(dmId)) return false;
|
|
//yk
|
|
if (dmId.Contains("..") || dmId.Contains("/") || dmId.Contains("\\"))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static bool IsSafeMsgId(string msgId)
|
|
{
|
|
if (string.IsNullOrEmpty(msgId)) return false;
|
|
if (msgId.Contains("..") || msgId.Contains("/") || msgId.Contains("\\"))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static async Task<bool> IsMemberOfDm(string id, string dmId)
|
|
{
|
|
if (!IsSafeDmId(dmId)) return false;
|
|
string memberPath = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/members/{id};{DOMAIN}";
|
|
return Fs.Exists(memberPath);
|
|
}
|
|
public static async Task<string> GetDmMessages(string id, string dmId, string startOffset, string countStr)
|
|
{
|
|
if (!await IsMemberOfDm(id, dmId)) return "error:forbidden";
|
|
|
|
string msgDir = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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 = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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 = "";
|
|
}
|
|
|
|
sb.Append(JsonSerializer.Serialize(msgObj, AppJsonSerializerContext.Default.Message));
|
|
fetched++;
|
|
}
|
|
}
|
|
sb.Append("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
|
|
public static async Task<string> GetDmKey(string id, string dmId)
|
|
{
|
|
if (!await IsMemberOfDm(id, dmId))
|
|
{
|
|
return "error:forbidden";
|
|
}
|
|
string path = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/keys/0/{id};{DOMAIN}";
|
|
if (!Fs.Exists(path))
|
|
{
|
|
return "";
|
|
}
|
|
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
|
|
}
|
|
|
|
public static async Task<string> UpdateDmKey(string id, string dmId, string key) //nie powinno byc takich rzeczy jak ta, jak juz powinnismy tworzyc nowy klucz, np. 1 i pozniej w wiadomosci wpisywac Message.key = "1" i nim szyfrowac
|
|
{
|
|
if (!await IsMemberOfDm(id, dmId))
|
|
{
|
|
return "error:forbidden";
|
|
}
|
|
if (key == null || key.Length > MAX_DM_KEY_SIZE)
|
|
{
|
|
return "error:dm.key.too.large";
|
|
}
|
|
string path = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/keys/0/{id};{DOMAIN}";
|
|
if (!Fs.Exists(path))
|
|
{
|
|
return "error:dm.key.not.found";
|
|
}
|
|
await Fs.WriteFile(path, Encoding.UTF8.GetBytes(key));
|
|
return "success:dm.key.updated";
|
|
}
|
|
|
|
public static async Task<string> DmInvite(string id, string targetId)
|
|
{
|
|
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
|
|
string tId = Account.Utils.GetValidIdOrZero(targetId);
|
|
|
|
string checkDmId = Account.Utils.GetDmId(id, DOMAIN, tId, domain);
|
|
if (Fs.Exists($"{ROOMS_DIR}/dms/{DOMAIN}/{checkDmId}"))
|
|
{
|
|
return "error:dm.already.exists";
|
|
}
|
|
|
|
if (!isLocal) //federation
|
|
{
|
|
await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{tId};{domain}", Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()));
|
|
return await Federation.Sender.DmInvite(id, domain, tId);
|
|
}
|
|
|
|
string id2 = tId;
|
|
if (id2 == "0" || string.IsNullOrEmpty(id2))
|
|
{
|
|
return "error:user.not.found";
|
|
}
|
|
|
|
if (id2 == id)
|
|
{
|
|
return "error:cant.invite.urself";
|
|
}
|
|
string inviteFile = ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/recv/{id}";
|
|
if (Fs.Exists(inviteFile))
|
|
{
|
|
return "info:user.already.invited";
|
|
}
|
|
|
|
byte[] timestamp = Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
|
|
await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{id2}", timestamp); //we should also save that this user invited someone, because listing sent invites is cool
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2);
|
|
await userLock2.WaitAsync();
|
|
try
|
|
{
|
|
await Fs.WriteFile(inviteFile, timestamp);
|
|
}
|
|
finally
|
|
{
|
|
userLock2.Release();
|
|
}
|
|
});
|
|
|
|
return "success:user.invited";
|
|
}
|
|
|
|
public static async Task<string> DmInviteRevoke(string id, string targetId)
|
|
{
|
|
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
|
|
string tId = Account.Utils.GetValidIdOrZero(targetId);
|
|
|
|
if (!isLocal) //federation
|
|
{
|
|
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{tId};{domain}");
|
|
return await Federation.Sender.DmInviteRevoke(id, domain, tId);
|
|
}
|
|
|
|
string id2 = tId;
|
|
if (id2 == "0" || string.IsNullOrEmpty(id2)) return "error:user.not.found";
|
|
|
|
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{id2}");
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2);
|
|
await userLock2.WaitAsync();
|
|
try
|
|
{
|
|
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/recv/{id}");
|
|
}
|
|
finally
|
|
{
|
|
userLock2.Release();
|
|
}
|
|
});
|
|
|
|
return "success:invite.revoked";
|
|
}
|
|
|
|
public static async Task<string> DmInviteDecline(string id, string targetId)
|
|
{
|
|
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
|
|
string tId = Account.Utils.GetValidIdOrZero(targetId);
|
|
|
|
if (!isLocal) //federation
|
|
{
|
|
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/recv/{tId};{domain}");
|
|
return await Federation.Sender.DmInviteDecline(id, domain, tId);
|
|
}
|
|
|
|
string id2 = tId;
|
|
if (id2 == "0" || string.IsNullOrEmpty(id2)) return "error:user.not.found";
|
|
|
|
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/recv/{id2}");
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2);
|
|
await userLock2.WaitAsync();
|
|
try
|
|
{
|
|
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/sent/{id}");
|
|
}
|
|
finally
|
|
{
|
|
userLock2.Release();
|
|
}
|
|
});
|
|
|
|
return "success:invite.declined";
|
|
}
|
|
|
|
public static async Task<string> DmCreate(string id, string body)
|
|
{
|
|
await createLock.WaitAsync();
|
|
try
|
|
{
|
|
Universal3String serializedBody = JsonSerializer.Deserialize( //1 is targetId, 2 is creators dm key, 3 is key for ID2
|
|
body,
|
|
AppJsonSerializerContext.Default.Universal3String
|
|
);
|
|
|
|
string targetId = serializedBody.string1;
|
|
bool isUserLocal = Account.Utils.IsUserLocal(targetId, out string domain);
|
|
string id2 = Account.Utils.GetValidIdOrZero(targetId);
|
|
|
|
if (id2 == "0" || string.IsNullOrEmpty(id2))
|
|
{
|
|
return "error:user.not.found";
|
|
}
|
|
|
|
string checkDmId = Account.Utils.GetDmId(id, DOMAIN, id2, domain);
|
|
if (Fs.Exists($"{ROOMS_DIR}/dms/{DOMAIN}/{checkDmId}"))
|
|
{
|
|
return "error:dm.already.exists";
|
|
}
|
|
|
|
Universal2String keys = JsonSerializer.Deserialize( //we need to pull keys before we do anything, because if user do NOT have them, dm creation will crash
|
|
await Account.Utils.GetUserKeys(id),
|
|
AppJsonSerializerContext.Default.Universal2String
|
|
);
|
|
|
|
if (isUserLocal)
|
|
{
|
|
|
|
string inviteSentFile = ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/sent/{id}";
|
|
if (!Fs.Exists(inviteSentFile))
|
|
{
|
|
return "error:cant.create.dm.without.invitation";
|
|
}
|
|
Fs.DeleteFile(inviteSentFile);
|
|
}
|
|
else //check sent on federated server
|
|
{
|
|
if (!await Federation.Sender.HasSentDmInvite(id2, domain, id))
|
|
{
|
|
return "error:cant.create.dm.without.invitation";
|
|
}
|
|
}
|
|
|
|
string inviteFile = $"{ACCOUNTS_DATA_DIR}/{id}/dminvites/recv/{id2}" + (isUserLocal ? "" : $";{domain}");
|
|
if (Fs.Exists(inviteFile))
|
|
{
|
|
|
|
try
|
|
{
|
|
Fs.DeleteFile(inviteFile); //remove invite bc now its accepted (error = no invite & no dm)
|
|
|
|
|
|
//best dmId creation ever
|
|
string dmId = checkDmId;
|
|
|
|
|
|
Fs.CreateDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/members");
|
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/members/{id};{DOMAIN}", Array.Empty<byte>());
|
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/members/{id2};{domain}", Array.Empty<byte>());
|
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/messages/last", Encoding.UTF8.GetBytes($"0"));
|
|
|
|
|
|
Message startingMessage = new();
|
|
startingMessage.attachment = "";
|
|
startingMessage.author = "0";
|
|
startingMessage.timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
|
|
startingMessage.type = "larp.info";
|
|
startingMessage.content =
|
|
"{blah(dm.begin.notice)}";
|
|
startingMessage.key = ""; //no key == NOT encrypted
|
|
startingMessage.pervious = "";
|
|
|
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/keys/0/{id};{DOMAIN}", //klucz dla uzytkownika tworzÄ…cego jest juz gotowy i ma zwykly zapis
|
|
Encoding.UTF8.GetBytes(serializedBody.string2));
|
|
|
|
|
|
|
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/keys/0/{id2};{domain}",
|
|
Encoding.UTF8.GetBytes($"SETUP:{keys.string2};{serializedBody.string3}")); //jezeli mamy setup to [1] to jest publiczny klucz drugiej osoby,
|
|
//a string 3 to zaszyfrowany klucz pokoju ktory musi odszyfrowac za pomoca swoich kluczy i tego publicznego
|
|
|
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/messages/0", Encoding.UTF8.GetBytes(
|
|
JsonSerializer.Serialize(startingMessage, AppJsonSerializerContext.Default.Message)
|
|
));
|
|
|
|
await Account.Utils.UpdateUserDm(id, dmId, "false",
|
|
startingMessage.timestamp);
|
|
|
|
if (isUserLocal)
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
SemaphoreSlim userLock = Account.Utils.GetUserLock(id2);
|
|
await userLock.WaitAsync();
|
|
try
|
|
{
|
|
await Account.Utils.UpdateUserDm(id2, dmId, "false",
|
|
startingMessage.timestamp);
|
|
}
|
|
finally
|
|
{
|
|
userLock.Release();
|
|
}
|
|
});
|
|
}
|
|
else //federacja
|
|
{
|
|
await Federation.Sender.AddToDm(id, domain, id2);
|
|
}
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
|
|
return "error:failed.accept.dm";
|
|
}
|
|
|
|
return "success:dm.accepted";
|
|
|
|
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
createLock.Release();
|
|
}
|
|
return "error:cant.create.dm.without.invitation";
|
|
}
|
|
|
|
public static async Task<string> DmMessageSend(string id, string body)
|
|
{
|
|
Universal4String serializedBody = JsonSerializer.Deserialize(
|
|
body,
|
|
AppJsonSerializerContext.Default.Universal4String
|
|
);
|
|
string dmId = serializedBody.string1;
|
|
string content = serializedBody.string2;
|
|
string attachment = serializedBody.string3;
|
|
string responded = serializedBody.string4;
|
|
|
|
if (!await IsMemberOfDm(id, dmId)) return "error:forbidden";
|
|
|
|
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}";
|
|
}
|
|
|
|
string msgDir = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/messages";
|
|
long last = 0;
|
|
if (Fs.Exists($"{msgDir}/last"))
|
|
{
|
|
last = long.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{msgDir}/last")));
|
|
}
|
|
long nextId = last + 1;
|
|
|
|
Message msg = new Message();
|
|
msg.author = id;
|
|
msg.timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
|
|
msg.type = "larp.text";
|
|
msg.content = content;
|
|
msg.attachment = attachment;
|
|
msg.key = "0"; //empty = no encryption, value = encryption key index
|
|
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 = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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"); // Clean it up before saving
|
|
}
|
|
}
|
|
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()));
|
|
|
|
string[] members = Fs.ReadDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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)
|
|
{
|
|
await Account.Utils.UpdateUserDm(memberId, dmId, "true", msg.timestamp);
|
|
await Utils.Utils.SendWsMessage(memberId, $"dm_message:{dmId}:read");
|
|
}
|
|
else
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
SemaphoreSlim userLock = Account.Utils.GetUserLock(memberId);
|
|
await userLock.WaitAsync();
|
|
try
|
|
{
|
|
await Account.Utils.UpdateUserDm(memberId, dmId, "false", msg.timestamp);
|
|
}
|
|
finally
|
|
{
|
|
userLock.Release();
|
|
}
|
|
await Utils.Utils.SendWsMessage(memberId, $"dm_message:{dmId}:unread");
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return $"success:{nextId}";
|
|
}
|
|
|
|
public static async Task<string> DmMessageDelete(string id, string body)
|
|
{
|
|
Universal2String serializedBody = JsonSerializer.Deserialize(
|
|
body,
|
|
AppJsonSerializerContext.Default.Universal2String
|
|
);
|
|
string dmId = serializedBody.string1;
|
|
string msgId = serializedBody.string2;
|
|
|
|
if (!IsSafeMsgId(msgId)) return "error:invalid.msg.id";
|
|
if (!await IsMemberOfDm(id, dmId)) return "error:forbidden";
|
|
|
|
string msgPath = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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.author != id) 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[] members = Fs.ReadDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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, $"dm_message:{dmId}:update");
|
|
}
|
|
}
|
|
return "success:message.deleted";
|
|
}
|
|
|
|
public static async Task<string> DmMessageReactAdd(string id, string body)
|
|
{
|
|
Universal3String serializedBody = JsonSerializer.Deserialize(
|
|
body,
|
|
AppJsonSerializerContext.Default.Universal3String
|
|
);
|
|
string dmId = serializedBody.string1;
|
|
string msgId = serializedBody.string2;
|
|
string reaction = serializedBody.string3;
|
|
|
|
if (!IsSafeMsgId(msgId)) return "error:invalid.msg.id";
|
|
if (!await IsMemberOfDm(id, dmId)) 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 = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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 = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/reactions/{msgId}";
|
|
if (!Fs.Exists(reactDir)) Fs.CreateDirectory(reactDir);
|
|
|
|
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()));
|
|
|
|
string[] members = Fs.ReadDirectory($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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, $"dm_message:{dmId}:update");
|
|
}
|
|
}
|
|
return "success:message.reacted";
|
|
}
|
|
|
|
public static async Task<string> DmMessageReactRemove(string id, string body)
|
|
{
|
|
Universal3String serializedBody = JsonSerializer.Deserialize(
|
|
body,
|
|
AppJsonSerializerContext.Default.Universal3String
|
|
);
|
|
string dmId = serializedBody.string1;
|
|
string msgId = serializedBody.string2;
|
|
string reactionId = serializedBody.string3;
|
|
|
|
if (!IsSafeMsgId(msgId)) return "error:invalid.msg.id";
|
|
if (!IsSafeMsgId(reactionId)) return "error:invalid.reaction.id";
|
|
if (!await IsMemberOfDm(id, dmId)) return "error:forbidden";
|
|
|
|
string reactPath = $"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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($"{ROOMS_DIR}/dms/{DOMAIN}/{dmId}/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, $"dm_message:{dmId}:update");
|
|
}
|
|
}
|
|
return "success:message.reaction.removed";
|
|
}
|
|
} |