add groups (TEST)
This commit is contained in:
parent
5c58492b60
commit
eb2388a290
10 changed files with 1502 additions and 100 deletions
135
.idea/.idea.Larpix/.idea/workspace.xml
generated
135
.idea/.idea.Larpix/.idea/workspace.xml
generated
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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("}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
522
LarpixServer/Room/Groups.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
538
LarpixServer/Room/Shared.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue