add groups (TEST)
All checks were successful
Server Build / publish (push) Successful in 29s
Voice Build / publish (push) Successful in 24s

This commit is contained in:
olcxja 2026-08-01 15:19:49 +02:00
commit eb2388a290
10 changed files with 1502 additions and 100 deletions

View file

@ -15,8 +15,14 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="09489c23-dd7d-4a2b-9db8-54d886979f2d" name="Changes" comment=""> <list default="true" id="09489c23-dd7d-4a2b-9db8-54d886979f2d" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/LarpixServer/Encryption/Encryption.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LarpixServer/Encryption/Encryption.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/.idea.Larpix/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Larpix/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LarpixServer/Filesystem/Fs.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LarpixServer/Filesystem/Fs.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/LarpixServer/Account/Requests.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LarpixServer/Account/Requests.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LarpixServer/Account/Utils.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LarpixServer/Account/Utils.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LarpixServer/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LarpixServer/Program.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LarpixServer/Room/Requests.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LarpixServer/Room/Requests.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LarpixServer/Utils/AppJsonSerializerContext.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LarpixServer/Utils/AppJsonSerializerContext.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LarpixServer/Utils/Jsons.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LarpixServer/Utils/Jsons.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LarpixServer/Utils/Utils.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LarpixServer/Utils/Utils.cs" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -50,61 +56,61 @@
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" /> <component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
<component name="NextEditCompletionFeaturesState"> <component name="NextEditCompletionFeaturesState">
<decayedCancelled> <decayedCancelled>
<entry key="MS100" value="1.0489204250883033" /> <entry key="MS100" value="1.0" />
<entry key="MS500" value="2.196212341840478" /> <entry key="MS500" value="1.000000000000203" />
<entry key="S2" value="5.150555470118407" /> <entry key="S2" value="1.0020625742104996" />
<entry key="S5" value="9.252385511122695" /> <entry key="S5" value="1.2908351894910703" />
<entry key="S10" value="12.86082885533096" /> <entry key="S10" value="3.1461354561082078" />
<entry key="S30" value="17.016791757566946" /> <entry key="S30" value="13.149255797303992" />
<entry key="S60" value="18.41317297887751" /> <entry key="S60" value="21.060681962690655" />
<entry key="M2" value="19.180981531482423" /> <entry key="M2" value="27.034477301328483" />
<entry key="M5" value="19.81423730222821" /> <entry key="M5" value="31.666867471213482" />
<entry key="M10" value="22.130957154262834" /> <entry key="M10" value="35.39097908787194" />
<entry key="M15" value="25.738068591580486" /> <entry key="M15" value="39.43638092696666" />
<entry key="M30" value="37.00412033161924" /> <entry key="M30" value="51.14003176549106" />
<entry key="H1" value="62.226461944000256" /> <entry key="H1" value="76.55907285266504" />
<entry key="H2" value="112.58648322051903" /> <entry key="H2" value="127.01845640253273" />
<entry key="H4" value="175.9285336578274" /> <entry key="H4" value="190.49712857937354" />
<entry key="D1" value="278.3707926614194" /> <entry key="D1" value="293.25914286117626" />
<entry key="W1" value="303.52496874927385" /> <entry key="W1" value="318.50762402539107" />
</decayedCancelled> </decayedCancelled>
<decayedSelected> <decayedSelected>
<entry key="MS100" value="0.0" /> <entry key="MS100" value="0.0" />
<entry key="MS500" value="0.0" /> <entry key="MS500" value="0.0" />
<entry key="S2" value="0.0" /> <entry key="S2" value="0.0" />
<entry key="S5" value="1.8131036209559798E-141" /> <entry key="S5" value="2.2439579850679373E-144" />
<entry key="S10" value="4.258055449328929E-71" /> <entry key="S10" value="1.4979846411321887E-72" />
<entry key="S30" value="3.491969465895373E-24" /> <entry key="S30" value="1.1442013438903205E-24" />
<entry key="S60" value="1.868681210344709E-12" /> <entry key="S60" value="1.069673475360738E-12" />
<entry key="M2" value="1.366997150818065E-6" /> <entry key="M2" value="1.0342501995942458E-6" />
<entry key="M5" value="0.004511355542479761" /> <entry key="M5" value="0.004035062416769761" />
<entry key="M10" value="0.0671835505563465" /> <entry key="M10" value="0.06353814812244588" />
<entry key="M15" value="0.16627617714386062" /> <entry key="M15" value="0.16020562661003204" />
<entry key="M30" value="0.5093703757419895" /> <entry key="M30" value="0.49998566201791655" />
<entry key="H1" value="2.071942554773281" /> <entry key="H1" value="2.052766935403745" />
<entry key="H2" value="6.826336109517314" /> <entry key="H2" value="6.794674156088677" />
<entry key="H4" value="13.708495488677372" /> <entry key="H4" value="13.676667125814342" />
<entry key="D1" value="25.51259601949395" /> <entry key="D1" value="25.502713937661543" />
<entry key="W1" value="28.471880589063524" /> <entry key="W1" value="28.4703048510474" />
</decayedSelected> </decayedSelected>
<decayedShown> <decayedShown>
<entry key="MS100" value="1.0343113866371232" /> <entry key="MS100" value="4.374993728539027E-10" />
<entry key="MS500" value="2.189320549162706" /> <entry key="MS500" value="0.013433695097634191" />
<entry key="S2" value="5.146010839796006" /> <entry key="S2" value="0.34230650923131567" />
<entry key="S5" value="9.248869409175045" /> <entry key="S5" value="0.9330151126386198" />
<entry key="S10" value="12.858328409560277" /> <entry key="S10" value="2.9346119570889306" />
<entry key="S30" value="17.015676120128635" /> <entry key="S30" value="13.063330864150512" />
<entry key="S60" value="18.412567991258015" /> <entry key="S60" value="21.01455719502268" />
<entry key="M2" value="19.18066743301237" /> <entry key="M2" value="27.010471000456437" />
<entry key="M5" value="19.818589402897985" /> <entry key="M5" value="31.66102214234888" />
<entry key="M10" value="22.197843512803257" /> <entry key="M10" value="35.449327027425014" />
<entry key="M15" value="25.90388942802016" /> <entry key="M15" value="39.59286601427399" />
<entry key="M30" value="37.51275217226305" /> <entry key="M30" value="51.63764702324305" />
<entry key="H1" value="64.29697741626724" /> <entry key="H1" value="78.60960202738265" />
<entry key="H2" value="119.41049208777486" /> <entry key="H2" value="133.8104018800184" />
<entry key="H4" value="189.63465621584874" /> <entry key="H4" value="204.17122209667906" />
<entry key="D1" value="303.88263663350585" /> <entry key="D1" value="318.76107066816263" />
<entry key="W1" value="331.99672892566826" /> <entry key="W1" value="346.9778035595358" />
</decayedShown> </decayedShown>
</component> </component>
<component name="ProjectColorInfo">{ <component name="ProjectColorInfo">{
@ -264,6 +270,11 @@
<workItem from="1780661424508" duration="36000" /> <workItem from="1780661424508" duration="36000" />
<workItem from="1780674482268" duration="55000" /> <workItem from="1780674482268" duration="55000" />
<workItem from="1781119829688" duration="604000" /> <workItem from="1781119829688" duration="604000" />
<workItem from="1784979485949" duration="2712000" />
<workItem from="1785334714728" duration="2945000" />
<workItem from="1785424629235" duration="481000" />
<workItem from="1785443538942" duration="1426000" />
<workItem from="1785579371714" duration="9184000" />
</task> </task>
<task id="LOCAL-00001" summary="First commit"> <task id="LOCAL-00001" summary="First commit">
<option name="closed" value="true" /> <option name="closed" value="true" />
@ -505,7 +516,23 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1780674501745</updated> <updated>1780674501745</updated>
</task> </task>
<option name="localTasksCounter" value="31" /> <task id="LOCAL-00031" summary="add message pinning">
<option name="closed" value="true" />
<created>1784934420228</created>
<option name="number" value="00031" />
<option name="presentableId" value="LOCAL-00031" />
<option name="project" value="LOCAL" />
<updated>1784934420228</updated>
</task>
<task id="LOCAL-00032" summary="fix few locks and unpin message">
<option name="closed" value="true" />
<created>1784980536325</created>
<option name="number" value="00032" />
<option name="presentableId" value="LOCAL-00032" />
<option name="project" value="LOCAL" />
<updated>1784980536325</updated>
</task>
<option name="localTasksCounter" value="33" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@ -526,8 +553,6 @@
</component> </component>
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" /> <option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
<MESSAGE value="Add license file" />
<MESSAGE value="Improve nonce caching" />
<MESSAGE value="Runner test" /> <MESSAGE value="Runner test" />
<MESSAGE value="Runner test 2" /> <MESSAGE value="Runner test 2" />
<MESSAGE value="Runner test 3" /> <MESSAGE value="Runner test 3" />
@ -551,7 +576,9 @@
<MESSAGE value="prevent user keys override, will add this later with password change" /> <MESSAGE value="prevent user keys override, will add this later with password change" />
<MESSAGE value="working messages!!@!@" /> <MESSAGE value="working messages!!@!@" />
<MESSAGE value="integer so unsafe!!! i need HEX!" /> <MESSAGE value="integer so unsafe!!! i need HEX!" />
<option name="LAST_COMMIT_MESSAGE" value="integer so unsafe!!! i need HEX!" /> <MESSAGE value="add message pinning" />
<MESSAGE value="fix few locks and unpin message" />
<option name="LAST_COMMIT_MESSAGE" value="fix few locks and unpin message" />
</component> </component>
<component name="XDebuggerManager"> <component name="XDebuggerManager">
<breakpoint-manager> <breakpoint-manager>

View file

@ -615,6 +615,21 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
return; return;
} }
public static async Task GetGroupPublicStorageEntry(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
{
if (!query.TryGetValue("id", out var idQuery)) return;
if (!query.TryGetValue("e", out var entry)) return;
string id = idQuery.ToString();
byte[] entryByte = await Room.Groups.GetGroupPublicStorageEntry(id, entry);
context.Response.ContentType = mimeTypes["unknown"];
context.Response.ContentLength = entryByte.Length;
await context.Response.Body.WriteAsync(entryByte, 0, entryByte.Length);
return;
}
public static async Task EncryptedRequest(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader) public static async Task EncryptedRequest(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
{ {
if (!query.TryGetValue("id", out var idQuery)) if (!query.TryGetValue("id", out var idQuery))
@ -648,8 +663,14 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
string[] actionRequests = { string[] actionRequests = {
"user/dm/invite", "user/dm/invite/revoke", "user/dm/invite/decline", "user/dm/create", "user/dm/invite", "user/dm/invite/revoke", "user/dm/invite/decline", "user/dm/create",
"dm/message/send", "dm/message/delete", "dm/message/react", "dm/key/update", "dm/message/send", "dm/message/delete", "dm/message/react", "dm/key/update", "dm/message/pin/toggle",
"user/storage/public/update", "user/storage/private/update", "dm/read" "dm/message/react/add", "dm/message/react/remove",
"user/storage/public/update", "user/storage/private/update", "dm/read",
"group/create", "user/group/list", "group/info",
"user/invite/accept", "user/invite/decline", "user/invite/revoke", "user/dm/list", "user/group/list",
"user/storage/public/update", "user/storage/private/update", "group/storage/public/update", "group/storage/private/update", "group/storage/private/getentry", "dm/read", "group/read",
"dm/message/send", "dm/message/delete", "dm/message/react/add", "dm/message/react/remove", "dm/message/pin/toggle",
"group/messages/get", "group/messages/pinned"
}; };
if (System.Array.IndexOf(actionRequests, serializedBody.string1) >= 0) if (System.Array.IndexOf(actionRequests, serializedBody.string1) >= 0)
{ {
@ -714,12 +735,54 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
, password)); , password));
break; break;
} }
case "group/storage/public/update":
{
Universal3String storageUpdate = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal3String
);
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Groups.UpdateGroupPublicStorageEntry(id, storageUpdate.string1, storageUpdate.string2, storageUpdate.string3)
, password));
break;
}
case "group/storage/private/update":
{
Universal3String storageUpdate = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal3String
);
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Groups.UpdateGroupPrivateStorageEntry(id, storageUpdate.string1, storageUpdate.string2, storageUpdate.string3)
, password));
break;
}
case "group/storage/private/getentry":
{
Universal2String storageUpdate = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Groups.GetGroupPrivateStorageEntry(id, storageUpdate.string1, storageUpdate.string2)
, password));
break;
}
case "user/dm/list": case "user/dm/list":
await context.Response.WriteAsync( await context.Response.WriteAsync(
Encryption.Encryption.EncryptString( Encryption.Encryption.EncryptString(
await Utils.GetUserDms(id) await Utils.GetUserDms(id)
, password)); , password));
break; break;
case "user/group/list":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Groups.GetUserGroups(id)
, password));
break;
case "user/dm/invite": case "user/dm/invite":
await context.Response.WriteAsync( await context.Response.WriteAsync(
Encryption.Encryption.EncryptString( Encryption.Encryption.EncryptString(
@ -741,51 +804,219 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
case "user/dm/create": case "user/dm/create":
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Requests.DmCreate(id, serializedBody.string2), password)); await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Requests.DmCreate(id, serializedBody.string2), password));
break; break;
case "group/create":
{
Universal2String groupCreate = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupCreate(id, groupCreate.string1, groupCreate.string2), password));
break;
}
case "group/info":
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInfo(serializedBody.string2), password));
break;
case "group/invite":
{
Universal2String payload = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInvite(id, payload.string1, payload.string2), password));
break;
}
case "group/invite/revoke":
{
Universal2String payload = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInviteRevoke(id, payload.string1, payload.string2), password));
break;
}
case "group/invite/decline":
{
Universal2String payload = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInviteDecline(id, payload.string1, payload.string2), password));
break;
}
case "group/join":
{
Universal2String payload = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupJoin(id, payload.string1, payload.string2), password));
break;
}
case "group/members":
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupMembers(id, serializedBody.string2), password));
break;
case "group/key/rotate":
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupKeyRotate(id, serializedBody.string2), password));
break;
case "group/key/get":
{
Universal2String payload = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupKeyGet(id, payload.string1, payload.string2), password));
break;
}
case "dm/message/send": case "dm/message/send":
await context.Response.WriteAsync( {
Encryption.Encryption.EncryptString( Universal4String msgSend = JsonSerializer.Deserialize(
await Room.Requests.DmMessageSend(id, serializedBody.string2) serializedBody.string2,
, password)); AppJsonSerializerContext.Default.Universal4String
break; );
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessageSend(id, "dms", msgSend.string1, msgSend.string2, msgSend.string3, msgSend.string4)
, password));
break;
}
case "dm/message/delete": case "dm/message/delete":
await context.Response.WriteAsync( {
Encryption.Encryption.EncryptString( Universal2String msgDel = JsonSerializer.Deserialize(
await Room.Requests.DmMessageDelete(id, serializedBody.string2) serializedBody.string2,
, password)); AppJsonSerializerContext.Default.Universal2String
break; );
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessageDelete(id, "dms", msgDel.string1, msgDel.string2)
, password));
break;
}
case "dm/message/pin/toggle": case "dm/message/pin/toggle":
await context.Response.WriteAsync( {
Encryption.Encryption.EncryptString( Universal2String msgPin = JsonSerializer.Deserialize(
await Room.Requests.DmMessagePinToggle(id, serializedBody.string2) serializedBody.string2,
, password)); AppJsonSerializerContext.Default.Universal2String
break; );
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessagePinToggle(id, "dms", msgPin.string1, msgPin.string2)
, password));
break;
}
case "dm/message/react/add": case "dm/message/react/add":
await context.Response.WriteAsync( {
Encryption.Encryption.EncryptString( Universal3String msgReact = JsonSerializer.Deserialize(
await Room.Requests.DmMessageReactAdd(id, serializedBody.string2) serializedBody.string2,
, password)); AppJsonSerializerContext.Default.Universal3String
break; );
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessageReactAdd(id, "dms", msgReact.string1, msgReact.string2, msgReact.string3)
, password));
break;
}
case "dm/message/react/remove": case "dm/message/react/remove":
await context.Response.WriteAsync( {
Encryption.Encryption.EncryptString( Universal3String msgReactRm = JsonSerializer.Deserialize(
await Room.Requests.DmMessageReactRemove(id, serializedBody.string2) serializedBody.string2,
, password)); AppJsonSerializerContext.Default.Universal3String
break; );
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessageReactRemove(id, "dms", msgReactRm.string1, msgReactRm.string2, msgReactRm.string3)
, password));
break;
}
case "dm/messages/get": case "dm/messages/get":
{ {
Universal3String msgGet = JsonSerializer.Deserialize( Universal3String msgGet = JsonSerializer.Deserialize(
serializedBody.string2, serializedBody.string2,
AppJsonSerializerContext.Default.Universal3String AppJsonSerializerContext.Default.Universal3String
); );
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Requests.GetDmMessages(id, msgGet.string1, msgGet.string3, msgGet.string2), password)); await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Shared.GetMessages(id, "dms", msgGet.string1, msgGet.string3, msgGet.string2), password));
break; break;
} }
case "dm/messages/pinned": case "dm/messages/pinned":
await context.Response.WriteAsync( await context.Response.WriteAsync(
Encryption.Encryption.EncryptString( Encryption.Encryption.EncryptString(
await Room.Requests.DmMessageGetPinned(id, serializedBody.string2) await Room.Shared.MessageGetPinned(id, "dms", serializedBody.string2)
, password)); , password));
break; break;
case "group/messages/get":
{
Universal3String msgGet = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal3String
);
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Shared.GetMessages(id, "groups", msgGet.string1, msgGet.string3, msgGet.string2), password));
break;
}
case "group/messages/pinned":
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessageGetPinned(id, "groups", serializedBody.string2)
, password));
break;
case "group/message/send":
{
Universal5String msgSend = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal5String
);
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessageSend(id, "groups", msgSend.string1, msgSend.string2, msgSend.string3, msgSend.string4, msgSend.string5)
, password));
break;
}
case "group/message/delete":
{
Universal2String msgDel = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessageDelete(id, "groups", msgDel.string1, msgDel.string2)
, password));
break;
}
case "group/message/react/add":
{
Universal3String msgReact = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal3String
);
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessageReactAdd(id, "groups", msgReact.string1, msgReact.string2, msgReact.string3)
, password));
break;
}
case "group/message/react/remove":
{
Universal3String msgReact = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal3String
);
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessageReactRemove(id, "groups", msgReact.string1, msgReact.string2, msgReact.string3)
, password));
break;
}
case "group/message/pin/toggle":
{
Universal2String msgPin = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await context.Response.WriteAsync(
Encryption.Encryption.EncryptString(
await Room.Shared.MessagePinToggle(id, "groups", msgPin.string1, msgPin.string2)
, password));
break;
}
case "dm/read": case "dm/read":
{ {
Universal2String readGet = JsonSerializer.Deserialize( Universal2String readGet = JsonSerializer.Deserialize(
@ -796,6 +1027,16 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("success:dm.read", password)); await context.Response.WriteAsync(Encryption.Encryption.EncryptString("success:dm.read", password));
break; break;
} }
case "group/read":
{
Universal2String readGet = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal2String
);
await Room.Groups.UpdateUserGroup(id, readGet.string1, "true", "");
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("success:group.read", password));
break;
}
case "dm/key/get": case "dm/key/get":
await context.Response.WriteAsync( await context.Response.WriteAsync(
Encryption.Encryption.EncryptString( Encryption.Encryption.EncryptString(
@ -948,10 +1189,43 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
string msgId = msgQuery.ToString(); string msgId = msgQuery.ToString();
string attId = attQuery.ToString(); string attId = attQuery.ToString();
// Check if user is in DM // Check if user is in room
if (!await Room.Requests.IsMemberOfDm(id, dmId)) return; if (!await Room.Shared.IsMemberOfRoom(id, "dms", dmId)) return;
string filePath = $"{LarpixServer.Utils.Utils.ROOMS_DIR}/dms/{LarpixServer.Utils.Utils.DOMAIN}/{dmId}/attachments/{msgId}/{attId}"; string filePath = $"{Room.Shared.GetRoomPath("dms", dmId)}/attachments/{msgId}/{attId}";
if (!Fs.Exists(filePath))
{
context.Response.StatusCode = 404;
return;
}
byte[] fileBytes = await Fs.ReadFile(filePath);
context.Response.ContentType = "application/octet-stream";
await context.Response.Body.WriteAsync(fileBytes, 0, fileBytes.Length);
}
public static async Task GetGroupAttachment(HttpContext context, Func<Task> next, IQueryCollection query)
{
if (!query.TryGetValue("id", out var idQuery)) return;
if (!query.TryGetValue("dm", out var dmQuery)) return;
if (!query.TryGetValue("msg", out var msgQuery)) return;
if (!query.TryGetValue("att", out var attQuery)) return;
if (!context.Request.Headers.TryGetValue("secret", out var secret)) return;
string id = Utils.GetValidIdOrZero(idQuery.ToString());
string password = await Utils.GetPassword(id);
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
string auth = await Utils.Auth(id, secretStr, password);
if (auth != Utils.LOGIN_SUCCESS) return;
string dmId = dmQuery.ToString();
string msgId = msgQuery.ToString();
string attId = attQuery.ToString();
if (!await Room.Shared.IsMemberOfRoom(id, "groups", dmId)) return;
string filePath = $"{Room.Shared.GetRoomPath("groups", dmId)}/attachments/{msgId}/{attId}";
if (!Fs.Exists(filePath)) if (!Fs.Exists(filePath))
{ {
context.Response.StatusCode = 404; context.Response.StatusCode = 404;

View file

@ -244,13 +244,19 @@ public class Utils
if (Fs.Exists(path)) if (Fs.Exists(path))
{ {
string[] invites = Fs.ReadDirectory(path); string[] invites = Fs.ReadDirectory(path);
bool first = true;
for (int i = 0; i < invites.Length; i++) for (int i = 0; i < invites.Length; i++)
{ {
string invite = invites[i]; string invite = invites[i];
string[] inviteArray = invite.Split('-'); int lastSemi = invite.LastIndexOf(';');
if (lastSemi == -1) continue;
string groupId = invite.Substring(0, lastSemi);
string userId = invite.Substring(lastSemi + 1);
if (!ulong.TryParse(userId, out _)) continue;
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}")); string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}"));
if (i > 0) groupBuilder.Append(','); if (!first) groupBuilder.Append(',');
groupBuilder.Append($"\"{inviteArray[0]}\": {{ \"timestamp\": {content}, \"receiver\": \"{inviteArray[1]}\" }}"); first = false;
groupBuilder.Append($"\"{groupId}\": {{ \"timestamp\": {content}, \"receiver\": \"{userId}\" }}");
} }
} }
groupBuilder.Append("}"); groupBuilder.Append("}");
@ -279,12 +285,19 @@ public class Utils
if (Fs.Exists(path)) if (Fs.Exists(path))
{ {
string[] invites = Fs.ReadDirectory(path); string[] invites = Fs.ReadDirectory(path);
bool first = true;
for (int i = 0; i < invites.Length; i++) for (int i = 0; i < invites.Length; i++)
{ {
string invite = invites[i]; string invite = invites[i];
string[] content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}")).Split(':'); int lastSemi = invite.LastIndexOf(';');
if (i > 0) groupBuilder.Append(','); if (lastSemi == -1) continue;
groupBuilder.Append($"\"{invite}\": {{ \"timestamp\": {content[1]}, \"sender\": \"{content[0]}\" }}"); string groupId = invite.Substring(0, lastSemi);
string userId = invite.Substring(lastSemi + 1);
if (!ulong.TryParse(userId, out _)) continue;
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}"));
if (!first) groupBuilder.Append(',');
first = false;
groupBuilder.Append($"\"{groupId}\": {{ \"timestamp\": {content}, \"sender\": \"{userId}\" }}");
} }
} }
groupBuilder.Append("}"); groupBuilder.Append("}");
@ -463,11 +476,16 @@ public class Utils
StringBuilder dmsBuilder = new StringBuilder("\"dms\":{"); StringBuilder dmsBuilder = new StringBuilder("\"dms\":{");
string[] dmFiles = Fs.ReadDirectory(path); string[] dmFiles = Fs.ReadDirectory(path);
bool first = true;
for (int i = 0; i < dmFiles.Length; i++) for (int i = 0; i < dmFiles.Length; i++)
{ {
if (i > 0) dmsBuilder.Append(','); string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{dmFiles[i]}"));
if (string.IsNullOrWhiteSpace(content)) continue;
if (!first) dmsBuilder.Append(',');
dmsBuilder.Append($"\"{dmFiles[i]}\":"); dmsBuilder.Append($"\"{dmFiles[i]}\":");
dmsBuilder.Append(Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{dmFiles[i]}"))); dmsBuilder.Append(content);
first = false;
} }
dmsBuilder.Append("}"); dmsBuilder.Append("}");

View file

@ -223,6 +223,9 @@ public class Program
case "/_larpix/dm/attachment/get": case "/_larpix/dm/attachment/get":
await Account.Requests.GetDmAttachment(context, next, query); await Account.Requests.GetDmAttachment(context, next, query);
return; return;
case "/_larpix/group/attachment/get":
await Account.Requests.GetGroupAttachment(context, next, query);
return;
case "/_larpix/federation": case "/_larpix/federation":
await Federation.Receiver.FederationRequest(context, next, query, reader); await Federation.Receiver.FederationRequest(context, next, query, reader);
return; return;
@ -232,6 +235,9 @@ public class Program
case "/_larpix/user/storage/public/getentry": case "/_larpix/user/storage/public/getentry":
await Account.Requests.GetUserPublicStorageEntry(context, next, query, reader); await Account.Requests.GetUserPublicStorageEntry(context, next, query, reader);
return; return;
case "/_larpix/group/storage/public/getentry":
await Account.Requests.GetGroupPublicStorageEntry(context, next, query, reader);
return;
} }
} }
catch (Exception ex) catch (Exception ex)

522
LarpixServer/Room/Groups.cs Normal file
View file

@ -0,0 +1,522 @@
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 Groups
{
public static async Task UpdateUserGroup(string id, string groupId, string isRead = "false", string timestamp = "")
{
if (await Account.Utils.NameFromId(id) == "") //if user account just got deleted
{
return;
}
string groupDir = $"{ACCOUNTS_DATA_DIR}/{id}/groups";
if (!Fs.Exists(groupDir))
{
Fs.CreateDirectory(groupDir);
}
if (!groupId.Contains(';'))
{
groupId = $"{groupId};{Utils.Utils.DOMAIN}";
}
string groupPath = $"{groupDir}/{groupId}";
Universal3String fileGroup = new Universal3String();
if (Fs.Exists(groupPath))
{
fileGroup = JsonSerializer.Deserialize(
Encoding.UTF8.GetString(await Fs.ReadFile(groupPath)),
AppJsonSerializerContext.Default.Universal3String
);
}
else
{
fileGroup.string1 = "";
fileGroup.string2 = "";
fileGroup.string3 = "";
}
fileGroup.string1 = groupId;
if (timestamp != "")
{
fileGroup.string2 = timestamp;
}
fileGroup.string3 = isRead;
await Fs.WriteFile(groupPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(fileGroup, AppJsonSerializerContext.Default.Universal3String)));
}
public static async Task<string> GroupCreate(string id, string name, string body)
{
await Room.Requests.createLock.WaitAsync();
try
{
string groupsDir = $"{ROOMS_DIR}/groups";
if (!Fs.Exists(groupsDir))
{
Fs.CreateDirectory(groupsDir);
}
ulong groupId = 0;
if (Fs.Exists($"{groupsDir}/last"))
{
groupId = ulong.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{groupsDir}/last")));
groupId++;
}
await Fs.WriteFile($"{groupsDir}/last", Encoding.UTF8.GetBytes(groupId.ToString()));
string groupPath = $"{groupsDir}/{DOMAIN}/{groupId}";
Fs.CreateDirectory(groupPath);
Fs.CreateDirectory($"{groupPath}/members");
Fs.CreateDirectory($"{groupPath}/messages");
Fs.CreateDirectory($"{groupPath}/keys");
Fs.CreateDirectory($"{groupPath}/keys/0");
await Fs.WriteFile($"{groupPath}/members/{id};{DOMAIN}", Array.Empty<byte>());
await Fs.WriteFile($"{groupPath}/messages/last", Encoding.UTF8.GetBytes("0"));
await Fs.WriteFile($"{groupPath}/keys/last", Encoding.UTF8.GetBytes("0"));
await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("false"));
await Fs.WriteFile($"{groupPath}/name", Encoding.UTF8.GetBytes(name));
// body is the key encrypted for the creator
await Fs.WriteFile($"{groupPath}/keys/0/{id};{DOMAIN}", Encoding.UTF8.GetBytes(body));
Message startingMessage = new();
startingMessage.attachment = "";
startingMessage.author = id;
startingMessage.timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
startingMessage.type = "larp.info.create";
startingMessage.content = "{blah(group.created)}";
startingMessage.key = ""; // no key == NOT encrypted
startingMessage.pervious = "";
await Fs.WriteFile($"{groupPath}/messages/0", Encoding.UTF8.GetBytes(
JsonSerializer.Serialize(startingMessage, AppJsonSerializerContext.Default.Message)
));
await UpdateUserGroup(id, groupId.ToString(), "true", startingMessage.timestamp);
return $"success:{groupId}";
}
catch (Exception)
{
return "error:failed.create.group";
}
finally
{
Room.Requests.createLock.Release();
}
}
public static async Task<string> GetUserGroups(string id)
{
string path = $"{ACCOUNTS_DATA_DIR}/{id}/groups";
if (!Fs.Exists(path))
{
return "{\"groups\":{}}";
}
StringBuilder groupsBuilder = new StringBuilder("\"groups\":{");
string[] groupFiles = Fs.ReadDirectory(path);
bool first = true;
for (int i = 0; i < groupFiles.Length; i++)
{
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{groupFiles[i]}"));
if (string.IsNullOrWhiteSpace(content)) continue;
if (!first) groupsBuilder.Append(',');
groupsBuilder.Append($"\"{groupFiles[i]}\":");
groupsBuilder.Append(content);
first = false;
}
groupsBuilder.Append("}");
return $"{{{groupsBuilder.ToString()}}}";
}
public static string GetGroupPath(string groupId)
{
string[] parts = groupId.Split(';');
string id = parts[0];
string domain = parts.Length > 1 ? parts[1] : DOMAIN;
return $"{ROOMS_DIR}/groups/{domain}/{id}";
}
public static async Task<string> GroupInfo(string groupId)
{
string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath))
{
return "error:group.not.found";
}
string name = "Unknown";
if (Fs.Exists($"{groupPath}/name"))
{
name = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/name"));
}
string memberCount = "0";
if (Fs.Exists($"{groupPath}/members"))
{
memberCount = Fs.ReadDirectory($"{groupPath}/members").Length.ToString();
}
string lastKeyIndex = "0";
if (Fs.Exists($"{groupPath}/keys/last"))
{
lastKeyIndex = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/keys/last"));
}
string newRequired = "false";
if (Fs.Exists($"{groupPath}/keys/newrequired"))
{
newRequired = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/keys/newrequired"));
}
var info = new Universal5String
{
string1 = groupId,
string2 = name,
string3 = memberCount,
string4 = lastKeyIndex,
string5 = newRequired
};
return JsonSerializer.Serialize(info, AppJsonSerializerContext.Default.Universal5String);
}
public static async Task<string> GroupInvite(string id, string groupId, string targetId)
{
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
string tId = Account.Utils.GetValidIdOrZero(targetId);
string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath)) return "error:group.not.found";
// Check if inviter is in group
if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group";
if (!isLocal) // federation
{
// TODO: Federation support for groups
return "error:federation.not.implemented";
}
string id2 = tId;
if (id2 == "0" || string.IsNullOrEmpty(id2)) return "error:user.not.found";
if (id2 == id) return "error:cant.invite.urself";
if (Fs.Exists($"{groupPath}/members/{id2};{DOMAIN}")) return "error:already.in.group";
string inviteFile = ACCOUNTS_DATA_DIR + $"/{id2}/groupinvites/recv/{groupId};{id}";
if (Fs.Exists(inviteFile)) return "info:user.already.invited";
byte[] timestamp = Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
Fs.CreateDirectory(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/sent");
await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/sent/{groupId};{id2}", timestamp);
_ = Task.Run(async () =>
{
SemaphoreSlim userLock2 = Account.Utils.GetUserLock(id2);
await userLock2.WaitAsync();
try
{
Fs.CreateDirectory(ACCOUNTS_DATA_DIR + $"/{id2}/groupinvites/recv");
await Fs.WriteFile(inviteFile, timestamp);
}
finally
{
userLock2.Release();
}
});
return "success:user.invited";
}
public static async Task<string> GroupInviteRevoke(string id, string groupId, string targetId)
{
string tId = Account.Utils.GetValidIdOrZero(targetId);
if (tId == "0") return "error:invalid.user";
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/sent/{groupId};{tId}");
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
if (isLocal)
{
_ = Task.Run(async () =>
{
SemaphoreSlim userLock2 = Account.Utils.GetUserLock(tId);
await userLock2.WaitAsync();
try
{
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{tId}/groupinvites/recv/{groupId};{id}");
}
finally
{
userLock2.Release();
}
});
}
return "success:invite.revoked";
}
public static async Task<string> GroupInviteDecline(string id, string groupId, string inviterId)
{
string iId = Account.Utils.GetValidIdOrZero(inviterId);
if (iId == "0") return "error:invalid.user";
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/recv/{groupId};{iId}");
bool isLocal = Account.Utils.IsUserLocal(inviterId, out string domain);
if (isLocal)
{
_ = Task.Run(async () =>
{
SemaphoreSlim userLock2 = Account.Utils.GetUserLock(iId);
await userLock2.WaitAsync();
try
{
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{iId}/groupinvites/sent/{groupId};{id}");
}
finally
{
userLock2.Release();
}
});
}
return "success:invite.declined";
}
public static async Task<string> GroupJoin(string id, string groupId, string inviterId)
{
string iId = Account.Utils.GetValidIdOrZero(inviterId);
if (iId == "0") return "error:invalid.user";
string inviteFile = ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/recv/{groupId};{iId}";
if (!Fs.Exists(inviteFile)) return "error:no.invite";
string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath)) return "error:group.not.found";
await Room.Requests.createLock.WaitAsync();
try
{
// Delete the invites
Fs.DeleteFile(inviteFile);
if (Account.Utils.IsUserLocal(inviterId, out string domain))
{
Fs.DeleteFile(ACCOUNTS_DATA_DIR + $"/{iId}/groupinvites/sent/{groupId};{id}");
}
// Add to members
await Fs.WriteFile($"{groupPath}/members/{id};{DOMAIN}", Array.Empty<byte>());
// Require new key
await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true"));
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
Message joinMsg = new();
joinMsg.attachment = "";
joinMsg.author = id;
joinMsg.timestamp = timestamp;
joinMsg.type = "larp.info.join";
joinMsg.content = "{blah(group.user.joined)}";
joinMsg.key = "";
joinMsg.pervious = "";
ulong msgId = ulong.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/messages/last")));
msgId++;
await Fs.WriteFile($"{groupPath}/messages/{msgId}", Encoding.UTF8.GetBytes(
JsonSerializer.Serialize(joinMsg, AppJsonSerializerContext.Default.Message)
));
await Fs.WriteFile($"{groupPath}/messages/last", Encoding.UTF8.GetBytes(msgId.ToString()));
await UpdateUserGroup(id, groupId, "true", timestamp);
return "success:joined";
}
finally
{
Room.Requests.createLock.Release();
}
}
public static async Task<string> GroupMembers(string id, string groupId)
{
string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath)) return "error:group.not.found";
if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group";
string[] memberFiles = Fs.ReadDirectory($"{groupPath}/members");
return JsonSerializer.Serialize(memberFiles, AppJsonSerializerContext.Default.StringArray);
}
public static async Task<string> GroupKeyRotate(string id, string payloadStr)
{
LarpixServer.Utils.Jsons.GroupKeyRotationPayload payload;
try
{
payload = JsonSerializer.Deserialize(payloadStr, AppJsonSerializerContext.Default.GroupKeyRotationPayload);
}
catch
{
return "error:invalid.payload";
}
if (payload == null || payload.groupId == null || payload.keys == null)
return "error:invalid.payload";
string groupId = payload.groupId;
string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath)) return "error:group.not.found";
if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group";
await Room.Requests.createLock.WaitAsync();
try
{
// 1. Check if newrequired is true
string newRequiredPath = $"{groupPath}/keys/newrequired";
if (!Fs.Exists(newRequiredPath) || Encoding.UTF8.GetString(await Fs.ReadFile(newRequiredPath)) != "true")
{
return "error:rotation.not.required";
}
// 2. Validate all members are included
string[] memberFiles = Fs.ReadDirectory($"{groupPath}/members");
if (payload.keys.Count != memberFiles.Length)
{
return "error:missing.keys";
}
foreach (string member in memberFiles)
{
if (!payload.keys.ContainsKey(member))
{
return "error:missing.keys";
}
}
// 3. Determine new key index
ulong newKeyIndex = 0;
string lastKeyPath = $"{groupPath}/keys/last";
if (Fs.Exists(lastKeyPath))
{
if (ulong.TryParse(Encoding.UTF8.GetString(await Fs.ReadFile(lastKeyPath)), out ulong lastKey))
{
newKeyIndex = lastKey + 1;
}
}
// 4. Save keys
string newKeyDir = $"{groupPath}/keys/{newKeyIndex}";
Fs.CreateDirectory(newKeyDir);
foreach (var kvp in payload.keys)
{
if (kvp.Value.Length > LarpixServer.Room.Requests.MAX_DM_KEY_SIZE)
{
return "error:key.too.large";
}
await Fs.WriteFile($"{newKeyDir}/{kvp.Key}", Encoding.UTF8.GetBytes(kvp.Value));
}
// 5. Update last key index and clear newrequired
await Fs.WriteFile(lastKeyPath, Encoding.UTF8.GetBytes(newKeyIndex.ToString()));
await Fs.WriteFile(newRequiredPath, Encoding.UTF8.GetBytes("false"));
return $"success:{newKeyIndex}";
}
finally
{
Room.Requests.createLock.Release();
}
}
public static async Task<string> GroupKeyGet(string id, string groupId, string keyIndex)
{
string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath)) return "error:group.not.found";
if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group";
string keyPath = $"{groupPath}/keys/{keyIndex}/{id};{DOMAIN}";
if (!Fs.Exists(keyPath))
{
return ""; // no key found
}
return Encoding.UTF8.GetString(await Fs.ReadFile(keyPath));
}
public static async Task<byte[]> GetGroupPublicStorageEntry(string groupId, string entry)
{
if (string.IsNullOrEmpty(entry) || entry.Contains("..") || entry.Contains("/") || entry.Contains("\\")) return new byte[] {};
string[] parts = groupId.Split(';');
string id = parts[0];
string domain = parts.Length > 1 ? parts[1] : Utils.Utils.DOMAIN;
string path = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/public/{entry}";
if (!Fs.Exists(path)) return new byte[] {};
return await Fs.ReadFile(path);
}
public static async Task<string> UpdateGroupPublicStorageEntry(string userId, string groupId, string entry, string contentBase64)
{
if (string.IsNullOrEmpty(entry) || entry.Contains("..") || entry.Contains("/") || entry.Contains("\\")) return "error:invalid.entry.name";
string[] parts = groupId.Split(';');
string id = parts[0];
string domain = parts.Length > 1 ? parts[1] : Utils.Utils.DOMAIN;
string memberPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/members/{userId};{Utils.Utils.DOMAIN}";
if (!Fs.Exists(memberPath)) return "error:not.a.member";
byte[] contentBytes;
try { contentBytes = Convert.FromBase64String(contentBase64); }
catch { contentBytes = Encoding.UTF8.GetBytes(contentBase64); }
string dirPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/public";
string path = $"{dirPath}/{entry}";
Fs.CreateDirectory(dirPath);
long currentTotalSize = Utils.Utils.GetDirectorySize(dirPath);
long fileExistingSize = Fs.Exists(path) ? new System.IO.FileInfo(path).Length : 0;
if (currentTotalSize - fileExistingSize + contentBytes.Length > Utils.Utils.PUBLIC_STORAGE_LIMIT) return "error:storage.limit.exceeded";
await Fs.WriteFile(path, contentBytes);
return "success:storage.entry.updated";
}
public static async Task<string> GetGroupPrivateStorageEntry(string userId, string groupId, string entry)
{
if (string.IsNullOrEmpty(entry) || entry.Contains("..") || entry.Contains("/") || entry.Contains("\\")) return "";
string[] parts = groupId.Split(';');
string id = parts[0];
string domain = parts.Length > 1 ? parts[1] : Utils.Utils.DOMAIN;
string memberPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/members/{userId};{Utils.Utils.DOMAIN}";
if (!Fs.Exists(memberPath)) return "error:not.a.member";
string path = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/private/{entry}";
if (!Fs.Exists(path)) return "";
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
}
public static async Task<string> UpdateGroupPrivateStorageEntry(string userId, string groupId, string entry, string contentBase64)
{
if (string.IsNullOrEmpty(entry) || entry.Contains("..") || entry.Contains("/") || entry.Contains("\\")) return "error:invalid.entry.name";
string[] parts = groupId.Split(';');
string id = parts[0];
string domain = parts.Length > 1 ? parts[1] : Utils.Utils.DOMAIN;
string memberPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/members/{userId};{Utils.Utils.DOMAIN}";
if (!Fs.Exists(memberPath)) return "error:not.a.member";
byte[] contentBytes;
try { contentBytes = Convert.FromBase64String(contentBase64); }
catch { contentBytes = Encoding.UTF8.GetBytes(contentBase64); }
string dirPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/private";
string path = $"{dirPath}/{entry}";
Fs.CreateDirectory(dirPath);
long currentTotalSize = Utils.Utils.GetDirectorySize(dirPath);
long fileExistingSize = Fs.Exists(path) ? new System.IO.FileInfo(path).Length : 0;
if (currentTotalSize - fileExistingSize + contentBytes.Length > Utils.Utils.PRIVATE_STORAGE_LIMIT) return "error:storage.limit.exceeded";
await Fs.WriteFile(path, contentBytes);
return "success:storage.entry.updated";
}
}

View file

@ -10,7 +10,7 @@ namespace LarpixServer.Room;
public class Requests public class Requests
{ {
public static SemaphoreSlim createLock = new SemaphoreSlim(1, 1); public static SemaphoreSlim createLock = new SemaphoreSlim(1, 1);
private const int MAX_DM_KEY_SIZE = 8192; public const int MAX_DM_KEY_SIZE = 8192;
private static bool IsSafeDmId(string dmId) private static bool IsSafeDmId(string dmId)
{ {

538
LarpixServer/Room/Shared.cs Normal file
View file

@ -0,0 +1,538 @@
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> 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")
{
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}";
}
string msgDir = $"{GetRoomPath(roomType, roomId)}/messages";
long nextId = 0;
Message msg = new Message();
await Room.Requests.createLock.WaitAsync();
try
{
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
{
Room.Requests.createLock.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";
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 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);
}
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);
await Room.Requests.createLock.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
{
Room.Requests.createLock.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;
await Room.Requests.createLock.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
{
Room.Requests.createLock.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";
}
}

View file

@ -2,14 +2,26 @@ using System.Text.Json.Serialization;
namespace LarpixServer.Utils.Jsons; namespace LarpixServer.Utils.Jsons;
using System.Collections.Generic;
[JsonSerializable(typeof(KeyExchangePayload))] [JsonSerializable(typeof(KeyExchangePayload))]
[JsonSerializable(typeof(KeyExchangePayloadClient))] [JsonSerializable(typeof(KeyExchangePayloadClient))]
[JsonSerializable(typeof(CaptchaPayloadClient))] [JsonSerializable(typeof(CaptchaPayloadClient))]
[JsonSerializable(typeof(Universal2String))] [JsonSerializable(typeof(Universal2String))]
[JsonSerializable(typeof(Universal3String))] [JsonSerializable(typeof(Universal3String))]
[JsonSerializable(typeof(Universal4String))] [JsonSerializable(typeof(Universal4String))]
[JsonSerializable(typeof(Universal5String))]
[JsonSerializable(typeof(ServerInfo))] [JsonSerializable(typeof(ServerInfo))]
[JsonSerializable(typeof(Message))] [JsonSerializable(typeof(Message))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(GroupKeyRotationPayload))]
[JsonSerializable(typeof(string[]))]
public partial class AppJsonSerializerContext : JsonSerializerContext public partial class AppJsonSerializerContext : JsonSerializerContext
{ {
} }
public class GroupKeyRotationPayload
{
public string groupId { get; set; }
public Dictionary<string, string> keys { get; set; }
}

View file

@ -25,6 +25,15 @@ public class Universal4String
public string string4 { get; set; } public string string4 { get; set; }
} }
public class Universal5String
{
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 class KeyExchangePayload public class KeyExchangePayload
{ {

View file

@ -357,10 +357,6 @@ public class Utils
Fs.CreateDirectory(ROOMS_DIR + "/freespaces"); Fs.CreateDirectory(ROOMS_DIR + "/freespaces");
} }
if (!Fs.Exists(ROOMS_DIR + "/lastgroup"))
{
await Fs.WriteFile(ROOMS_DIR + "/lastgroup", Encoding.UTF8.GetBytes("0"));
}
if (!Fs.Exists(ROOMS_DIR + "/lastspace")) if (!Fs.Exists(ROOMS_DIR + "/lastspace"))
{ {
await Fs.WriteFile(ROOMS_DIR + "/lastspace", Encoding.UTF8.GetBytes("0")); await Fs.WriteFile(ROOMS_DIR + "/lastspace", Encoding.UTF8.GetBytes("0"));