LarpixServer/LarpixServer/Room/Requests.cs
olcxja e33554f369
All checks were successful
Server Build / publish (push) Successful in 30s
Voice Build / publish (push) Successful in 26s
working messages!!@!@
2026-06-02 12:53:32 +02:00

427 lines
No EOL
15 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 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}\":");
sb.Append(Encoding.UTF8.GetString(await Fs.ReadFile(p)));
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)
{
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)
{
Universal3String serializedBody = JsonSerializer.Deserialize(
body,
AppJsonSerializerContext.Default.Universal3String
);
string dmId = serializedBody.string1;
string content = serializedBody.string2;
string attachment = serializedBody.string3;
if (!await IsMemberOfDm(id, dmId)) return "error:forbidden";
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"; // Flag for encrypted content
msg.pervious = "";
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}");
}
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}");
});
}
}
}
return "success:message.sent";
}
}