diff --git a/.idea/.idea.MiniUtils/.idea/vcs.xml b/.idea/.idea.MiniUtils/.idea/vcs.xml
index 94a25f7..0347848 100644
--- a/.idea/.idea.MiniUtils/.idea/vcs.xml
+++ b/.idea/.idea.MiniUtils/.idea/vcs.xml
@@ -2,5 +2,7 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
+ <mapping directory="$PROJECT_DIR$/LibMatrix" vcs="Git" />
+ <mapping directory="$PROJECT_DIR$/LibMatrix/ArcaneLibs" vcs="Git" />
</component>
</project>
\ No newline at end of file
diff --git a/LibMatrix b/LibMatrix
-Subproject 28adb35ab9b6905eebcd83b6caa1b12d49b26be
+Subproject 6af19f2d27739e9cecaf6bab13a92b5705aba2f
diff --git a/MiniUtils.CSync/Emojis.cs b/MiniUtils.CSync/Emojis.cs
new file mode 100644
index 0000000..aef8904
--- /dev/null
+++ b/MiniUtils.CSync/Emojis.cs
@@ -0,0 +1,17 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace MiniUtils.Classes;
+
+[SuppressMessage("ReSharper", "UnusedMember.Local")]
+public class Emojis {
+ // Useful page: https://www.compart.com/en/unicode
+
+ public const string ThumbsUp = "\ud83d\udc4d\ufe0e";
+ public const string Recycle = "\u267b\ufe0e";
+ public const string Bullseye = "\u25ce\ufe0e";
+ public const string RightArrowWithTail = "\u21a3\ufe0e";
+ public const string Prohibited = "\ud83d\udec7\ufe0e";
+ public const string Wastebasket = "\ud83d\uddd1\ufe0e";
+ public const string Hourglass = "\u231b\ufe0e";
+ public const string Checkmark = "\u2705\ufe0e";
+}
\ No newline at end of file
diff --git a/MiniUtils.CSync/MiniUtils.CSync.csproj b/MiniUtils.CSync/MiniUtils.CSync.csproj
new file mode 100644
index 0000000..c76d186
--- /dev/null
+++ b/MiniUtils.CSync/MiniUtils.CSync.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk.Worker">
+
+ <PropertyGroup>
+ <TargetFramework>net10.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UserSecretsId>dotnet-MiniUtils.CSync-9083faba-cbe2-48f2-b201-eca73a4050e9</UserSecretsId>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
+ <ProjectReference Include="..\LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/MiniUtils.CSync/Program.cs b/MiniUtils.CSync/Program.cs
new file mode 100644
index 0000000..a86852c
--- /dev/null
+++ b/MiniUtils.CSync/Program.cs
@@ -0,0 +1,10 @@
+using LibMatrix.Services;
+using LibMatrix.Utilities.Bot;
+using MiniUtils.CSync;
+
+var builder = Host.CreateApplicationBuilder(args);
+builder.Services.AddHostedService<Worker>();
+builder.Services.AddRoryLibMatrixServices().AddMatrixBot();
+
+var host = builder.Build();
+host.Run();
diff --git a/MiniUtils.CSync/Properties/launchSettings.json b/MiniUtils.CSync/Properties/launchSettings.json
new file mode 100644
index 0000000..9985ad5
--- /dev/null
+++ b/MiniUtils.CSync/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "MiniUtils.CSync": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/MiniUtils.CSync/Worker.cs b/MiniUtils.CSync/Worker.cs
new file mode 100644
index 0000000..b2f8d58
--- /dev/null
+++ b/MiniUtils.CSync/Worker.cs
@@ -0,0 +1,48 @@
+using LibMatrix;
+using LibMatrix.EventTypes.Spec.State.Policy;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using LibMatrix.RoomTypes;
+using MiniUtils.Classes;
+
+namespace MiniUtils.CSync;
+
+public class Worker(ILogger<Worker> logger, AuthenticatedHomeserverGeneric hs) : BackgroundService {
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
+ while (!stoppingToken.IsCancellationRequested) {
+ var cme = hs.GetRoom("!fTjMjIzNKEsFlUIiru:neko.dev");
+ var targetRoom = hs.GetRoom("!kiP76YI0AlDqXjcKj2nM_Bq30GCgGWehndSt3iyA85E");
+
+ var policies = (await cme.GetFullStateAsListAsync()).Where(x => x is { Type: UserPolicyRuleEventContent.EventId, RawContent.Count: > 1 })
+ .Select(x => x.ContentAs<UserPolicyRuleEventContent>())
+ .ToList();
+ var members = await targetRoom.GetMemberIdsListAsync(membership: "join");
+ var intersected = members.Where(x => policies.Any(p => p!.Entity == x)).ToList();
+ logger.LogInformation("Found {count} members matching policies", intersected.Count);
+
+ if (intersected.Count > 0) {
+ await targetRoom.BulkSendEventsAsync(intersected.Select(x => new StateEvent() {
+ Type = RoomMemberEventContent.EventId,
+ StateKey = x,
+ TypedContent = new RoomMemberEventContent() {
+ Membership = "ban",
+ Reason = "spam"
+ }
+ }));
+ await targetRoom.SendMessageEventAsync(new MessageBuilder()
+ .WithBody($"[Pagination helper - {Emojis.Wastebasket}] Banned {intersected.Count} users")
+ .Build());
+ // await RedactEventsAsync()
+ }
+
+ await Task.Delay(5000, stoppingToken);
+ }
+ }
+
+ private async Task RedactEventsAsync(List<string> userIds, GenericRoom room) {
+ for (int i = 0; i < 10; i++) {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/MiniUtils/appsettings.Development.json b/MiniUtils.CSync/appsettings.Development.json
index 4cfb975..4cfb975 100644
--- a/MiniUtils/appsettings.Development.json
+++ b/MiniUtils.CSync/appsettings.Development.json
diff --git a/MiniUtils.CSync/appsettings.json b/MiniUtils.CSync/appsettings.json
new file mode 100644
index 0000000..b2dcdb6
--- /dev/null
+++ b/MiniUtils.CSync/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/MiniUtils.Core/MiniUtils.Core.csproj b/MiniUtils.Core/MiniUtils.Core.csproj
index 6e9363f..d0b0f4c 100644
--- a/MiniUtils.Core/MiniUtils.Core.csproj
+++ b/MiniUtils.Core/MiniUtils.Core.csproj
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-preview.3.25171.5" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
diff --git a/MiniUtils.sln b/MiniUtils.sln
index 188112f..2c06257 100644
--- a/MiniUtils.sln
+++ b/MiniUtils.sln
@@ -44,6 +44,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniUtils.Core", "MiniUtils.Core\MiniUtils.Core.csproj", "{5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Federation", "LibMatrix\LibMatrix.Federation\LibMatrix.Federation.csproj", "{7BBE090D-3EB6-47AB-B344-B4B32195E3BA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.FederationTest", "LibMatrix\Utilities\LibMatrix.FederationTest\LibMatrix.FederationTest.csproj", "{3405E10B-B86C-4495-B501-DBE5F4F99BF6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniUtils.CSync", "MiniUtils.CSync\MiniUtils.CSync.csproj", "{9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -294,6 +300,42 @@ Global
{5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Release|x64.Build.0 = Release|Any CPU
{5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Release|x86.ActiveCfg = Release|Any CPU
{5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Release|x86.Build.0 = Release|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Debug|x64.Build.0 = Debug|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Debug|x86.Build.0 = Debug|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Release|x64.ActiveCfg = Release|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Release|x64.Build.0 = Release|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Release|x86.ActiveCfg = Release|Any CPU
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA}.Release|x86.Build.0 = Release|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Debug|x64.Build.0 = Debug|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Debug|x86.Build.0 = Debug|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Release|x64.ActiveCfg = Release|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Release|x64.Build.0 = Release|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Release|x86.ActiveCfg = Release|Any CPU
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6}.Release|x86.Build.0 = Release|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Debug|x64.Build.0 = Debug|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Debug|x86.Build.0 = Debug|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Release|x64.ActiveCfg = Release|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Release|x64.Build.0 = Release|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Release|x86.ActiveCfg = Release|Any CPU
+ {9AA6A2E0-0895-4964-BDB4-CC0BE85C5375}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -308,15 +350,17 @@ Global
{7CF6BBC9-2176-452A-A55F-A315F611923A} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
{C1F04A37-F946-4AA9-872D-A38A289F41BE} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
{545A47FC-6BAA-4C92-BAD1-C1ED413077BC} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {59745535-3681-423D-AE8F-5C08D4671ADC} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {6FE1B566-08DF-4BC0-AC2F-F9176AA17087} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {F04E9976-C303-4DEA-AA8D-EEDE6DF44069} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {A61BAD5E-77AC-49A0-8028-5E84EF990987} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {E6F92ED7-6EE2-4D71-8962-15613CD379BC} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {3D5DDCB4-3840-4ACE-AB17-A217F3A27F50} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {272B0A44-B985-47FF-8C93-32A77F5919C9} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {D852D486-F1DA-48A6-BBAF-43FF3396527D} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {6C5E7FB8-D6E8-4CBD-95FC-89EAB773472A} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
- {E526424D-ADC7-41DB-AD63-E359AC2954D9} = {17E2FB3F-0F61-3CDC-2874-2686F1726316}
+ {59745535-3681-423D-AE8F-5C08D4671ADC} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {6FE1B566-08DF-4BC0-AC2F-F9176AA17087} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {F04E9976-C303-4DEA-AA8D-EEDE6DF44069} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {A61BAD5E-77AC-49A0-8028-5E84EF990987} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {E6F92ED7-6EE2-4D71-8962-15613CD379BC} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {3D5DDCB4-3840-4ACE-AB17-A217F3A27F50} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {272B0A44-B985-47FF-8C93-32A77F5919C9} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {D852D486-F1DA-48A6-BBAF-43FF3396527D} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {6C5E7FB8-D6E8-4CBD-95FC-89EAB773472A} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {E526424D-ADC7-41DB-AD63-E359AC2954D9} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {7BBE090D-3EB6-47AB-B344-B4B32195E3BA} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
+ {3405E10B-B86C-4495-B501-DBE5F4F99BF6} = {6C4179B9-BFAA-0403-502F-9DAF28C26A6E}
EndGlobalSection
EndGlobal
diff --git a/MiniUtils.sln.DotSettings.user b/MiniUtils.sln.DotSettings.user
index f543792..caf48e9 100644
--- a/MiniUtils.sln.DotSettings.user
+++ b/MiniUtils.sln.DotSettings.user
@@ -3,16 +3,24 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfigurationBinder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8feb37d1c2bc4bb7aba846da979b825aaf20_003F20_003Fa7159e0d_003FConfigurationBinder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultInterpolatedStringHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff1b929573c264d7a81f261ae2f951019d19e00_003F38_003F707b550d_003FDefaultInterpolatedStringHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnumerable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe443585619b64bcd8252486eca6648c078a00_003F5c_003F555f35de_003FEnumerable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnumerable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F913c179e36c64426ac5fcfffcc9268b278a00_003F89_003F537d998d_003FEnumerable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F97_003Ffb30ee1f_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff1b929573c264d7a81f261ae2f951019d19e00_003Fc1_003F72e4c91c_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fba81a6af46624b56ac6210bdbcc99af2d19e00_003F7d_003F77f9c40b_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHashtable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003Fd2_003F20632896_003FHashtable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdc2c4f9d0a9d4546b54cbaedf35715951a1e00_003F2f_003F4be0d618_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonContent_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F82a6262f326341f784e01e686c8860131b800_003F85_003F1952a932_003FJsonContent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonSerializerOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4a803072a1ec4b5cb542160d01f4125a16d400_003Fcc_003F8618e224_003FJsonSerializerOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F65_003Fb77a719c_003FList_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff1b929573c264d7a81f261ae2f951019d19e00_003F2f_003F707c45aa_003FList_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObjectExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8fd5e96d6574456095123be1ecfbdfa914200_003Fe2_003F3561a383_003FObjectExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APipeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6abeb72143fa4b84be432034a29f92d229400_003Fec_003F8fb684db_003FPipeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARune_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff1b929573c264d7a81f261ae2f951019d19e00_003F50_003F2cf1d657_003FRune_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionHostedServiceExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff13297a632424a6abffea4dd75a36a75d128_003Ff9_003Fb35aae11_003FServiceCollectionHostedServiceExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F86_003F8b4aa64e_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff1b929573c264d7a81f261ae2f951019d19e00_003Fc5_003Fbce9c992_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStreamContent_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fed59b04e6e114d64aa0f92cbdff4d26e1a1e00_003F1a_003Fa146de84_003FStreamContent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATaskAwaiter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F10_003F04572d58_003FTaskAwaiter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
- <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe443585619b64bcd8252486eca6648c078a00_003Faf_003Fe6779903_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
\ No newline at end of file
+ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelpers_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff1b929573c264d7a81f261ae2f951019d19e00_003F5c_003F317c3cdc_003FThrowHelpers_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe443585619b64bcd8252486eca6648c078a00_003Faf_003Fe6779903_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUtf8JsonWriter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4a803072a1ec4b5cb542160d01f4125a16d400_003Fdf_003F9b020bcf_003FUtf8JsonWriter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
\ No newline at end of file
diff --git a/MiniUtils/Commands/DumpTimelineCommand.cs b/MiniUtils/Commands/DumpTimelineCommand.cs
index 4ee53c9..1139fb6 100644
--- a/MiniUtils/Commands/DumpTimelineCommand.cs
+++ b/MiniUtils/Commands/DumpTimelineCommand.cs
@@ -1,6 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using LibMatrix;
+using LibMatrix.Responses;
using LibMatrix.Services;
using LibMatrix.Utilities.Bot.Interfaces;
diff --git a/MiniUtils/Commands/IgnoreCommand.cs b/MiniUtils/Commands/IgnoreCommand.cs
index 4206b72..1bd4de3 100644
--- a/MiniUtils/Commands/IgnoreCommand.cs
+++ b/MiniUtils/Commands/IgnoreCommand.cs
@@ -44,23 +44,28 @@ public class IgnoreCommand(IgnoreListManager ignoreListManager) : ICommand {
await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
}
else if (ctx.Args is ["disable", .. var itemsToDisable]) {
- var count = await ignoreListManager.MoveList(false, itemsToDisable);
+ var count = await ignoreListManager.MoveList(false, itemsToDisable.Where(x => x.StartsWith('@')));
await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
}
else if (ctx.Args is ["enable", .. var itemsToEnable]) {
- var count = await ignoreListManager.MoveList(true, itemsToEnable);
+ var count = await ignoreListManager.MoveList(true, itemsToEnable.Where(x => x.StartsWith('@')));
await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
}
else if (ctx.Args is ["add", .. var itemsToAdd]) {
- var count = await ignoreListManager.AddList(itemsToAdd);
+ var count = await ignoreListManager.AddList(itemsToAdd.Where(x => x.StartsWith('@')));
+ await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
+ }
+
+ else if (ctx.Args is ["remove", .. var itemsToRemove]) {
+ var count = await ignoreListManager.RemoveList(itemsToRemove.Where(x => x.StartsWith('@')));
await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
}
}
private async Task Summarize(CommandContext ctx, IgnoredUserListEventContentWithDisabled ignoreList) {
- var msb = new MessageBuilder()
- .WithBody($"Ignored users: {ignoreList.IgnoredUsers.Count}").WithNewline()
- .WithBody($"Disabled ignores: {ignoreList.DisabledIgnoredUsers.Count}").WithNewline();
- await ctx.Room.SendMessageEventAsync(msb.Build());
- }
- }
\ No newline at end of file
+ var msb = new MessageBuilder()
+ .WithBody($"Ignored users: {ignoreList.IgnoredUsers.Count}").WithNewline()
+ .WithBody($"Disabled ignores: {ignoreList.DisabledIgnoredUsers.Count}").WithNewline();
+ await ctx.Room.SendMessageEventAsync(msb.Build());
+ }
+}
\ No newline at end of file
diff --git a/MiniUtils/Commands/KickACLedCommand.cs b/MiniUtils/Commands/KickACLedCommand.cs
new file mode 100644
index 0000000..9ea8ec0
--- /dev/null
+++ b/MiniUtils/Commands/KickACLedCommand.cs
@@ -0,0 +1,73 @@
+using System.Collections.Frozen;
+using ArcaneLibs.Extensions;
+using LibMatrix;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Filters;
+using LibMatrix.Helpers;
+using LibMatrix.RoomTypes;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MiniUtils.Classes;
+using MiniUtils.Services;
+
+namespace MiniUtils.Commands;
+
+public class KickACLedCommand(IgnoreListManager ignoreListManager) : ICommand {
+ public string Name => "kick acled users";
+
+ public string[]? Aliases => [];
+
+ public string Description => "Kick all users targetted by server ACLs";
+
+ public bool Unlisted => false;
+
+ public async Task Invoke(CommandContext ctx) {
+ if (ctx.Args is ["banned"])
+ await RedactUsers(ctx, await ctx.Room.GetMemberIdsListAsync("ban"));
+ else if (ctx.Args is [.. var senders]) {
+ var sendersSet = senders.ToFrozenSet();
+ await RedactUsers(ctx, sendersSet);
+ }
+ }
+
+ private async Task RedactUsers(CommandContext ctx, FrozenSet<string> senders) {
+ var count = 0;
+ var subCount = 0;
+ List<Task> tasks = [];
+ // await foreach (var resp in ctx.Room.GetManyMessagesAsync(filter: filter.ToJson(false, ignoreNull: true), chunkSize: 1000)) {
+ // foreach (var chunk in resp.Chunk.Chunk(49)) {
+ // foreach (var evt in chunk) {
+ // if (!senders.Contains(evt.Sender!)) continue;
+ // tasks.Add(RedactEvent(ctx.Room, evt.EventId!));
+ // count++;
+ // subCount++;
+ // }
+ //
+ // if (subCount >= 40) {
+ // await ctx.Room.SendMessageEventAsync(new MessageBuilder()
+ // .WithBody(
+ // $"[{Emojis.Hourglass}] {Emojis.Recycle} {count} ({Emojis.Checkmark} {tasks.Count(t => t.IsCompletedSuccessfully)} {Emojis.Prohibited} {tasks.Count(t => t.IsFaulted)} {Emojis.Hourglass} {tasks.Count(t => t.Status == TaskStatus.Running)})")
+ // .Build());
+ // // await Task.WhenAll(tasks);
+ // subCount = 0;
+ // }
+ // }
+ // }
+
+ var acls = await ctx.Room.GetStateOrNullAsync<RoomServerAclEventContent>(RoomServerAclEventContent.EventId);
+ if (acls == null) {
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder().WithBody("No ACLs found.").Build());
+ return;
+ }
+
+ await foreach (var resp in ctx.Room.GetMembersEnumerableAsync()) {
+ var serverName = resp.StateKey!.Split(':',2)[1];
+ if (acls.DenyRegexes?.Any(x => x.IsMatch(serverName)) ?? false) {
+ Console.WriteLine("Kicking {0} from {1} due to ACL match: {2}", resp.StateKey, ctx.Room.RoomId, acls.DenyRegexes.First(x => x.IsMatch(serverName)));
+ }
+ }
+
+ await Task.WhenAll(tasks);
+
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder().WithBody($"{Emojis.Recycle} {count}").Build());
+ }
+}
\ No newline at end of file
diff --git a/MiniUtils/Commands/MakePolicyListCommand.cs b/MiniUtils/Commands/MakePolicyListCommand.cs
index 40b0695..b5639aa 100644
--- a/MiniUtils/Commands/MakePolicyListCommand.cs
+++ b/MiniUtils/Commands/MakePolicyListCommand.cs
@@ -8,9 +8,9 @@ using LibMatrix.Utilities.Bot.Interfaces;
namespace MiniUtils.Commands;
public class MakePolicyListCommand() : ICommand {
- public string Name => "makepolicylist";
+ public string Name => "make policy list";
- public string[]? Aliases => ["make policy list"];
+ public string[]? Aliases => ["makepolicylist"];
public string Description => "Make a new policy list";
diff --git a/MiniUtils/Commands/MakeRoomCommand.cs b/MiniUtils/Commands/MakeRoomCommand.cs
new file mode 100644
index 0000000..068bca5
--- /dev/null
+++ b/MiniUtils/Commands/MakeRoomCommand.cs
@@ -0,0 +1,235 @@
+using ArcaneLibs.Extensions;
+using LibMatrix.EventTypes.Common;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Helpers;
+using LibMatrix.Responses;
+using LibMatrix.RoomTypes;
+using LibMatrix.Utilities.Bot.Interfaces;
+
+namespace MiniUtils.Commands;
+
+public class MakeRoomCommand() : ICommand {
+ public string Name => "make room";
+
+ public string[]? Aliases => ["makeroom", "create room", "createroom"];
+
+ public string Description => "Make a new room";
+
+ public bool Unlisted => false;
+
+ public async Task Invoke(CommandContext ctx) {
+ if (ctx.Args.Length == 0) {
+ await ctx.Room.SendMessageEventAsync(
+ new MessageBuilder()
+ .WithTable(tb => {
+ tb.WithTitle("~create room", 3);
+ tb.WithRow(rb => {
+ rb.WithCell("Argument")
+ .WithCell("Alternatives")
+ .WithCell("Description");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--alias <localpart>")
+ .WithCell("")
+ .WithCell("Set the room alias");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--avatar-url <url>")
+ .WithCell("")
+ .WithCell("Set the room avatar URL");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--copy-avatar [room]")
+ .WithCell("")
+ .WithCell("Copy the avatar from another room (or current room if unspecified)");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--copy-powerlevels [room]")
+ .WithCell("")
+ .WithCell("Copy the power levels from another room (or current room if unspecified)");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--invite-admin <user>")
+ .WithCell("")
+ .WithCell("Invite a user as an admin");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--invite <user>")
+ .WithCell("")
+ .WithCell("Invite a user");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--name <name>")
+ .WithCell("")
+ .WithCell("Set the room name");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--topic <topic>")
+ .WithCell("")
+ .WithCell("Set the room topic");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--federate <true|false>")
+ .WithCell("")
+ .WithCell("Set whether the room is federatable");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--join-rule <rule>")
+ .WithCell("""
+ --public
+ --invite-only
+ --knock
+ --restricted
+ --knock_restricted
+ --private
+ """)
+ .WithCell("Set the room join rule to public, invite-only, knock, restricted, knock-restricted or private");
+ });
+ tb.WithRow(rb => {
+ rb.WithCell("--history-visibility <visibility>")
+ .WithCell("""
+ --shared
+ --invited
+ --joined
+ --world_readable
+ """)
+ .WithCell("Set the room history visibility to shared, invited, joined or world_readable");
+ });
+ })
+ .Build()
+ );
+ return;
+ }
+
+ var rb = new RoomBuilder() { };
+
+ for (int i = 0; i < ctx.Args.Length; i++) {
+ switch (ctx.Args[i]) {
+ case "--alias":
+ rb.AliasLocalPart = ctx.Args[++i];
+ break;
+ case "--avatar-url":
+ rb.Avatar!.Url = ctx.Args[++i];
+ break;
+ case "--copy-avatar": {
+ var room = await GetRoomByArgument(ctx, ctx.Args[i + 1]);
+ if (room != ctx.Room) i++;
+ rb.Avatar = await room.GetAvatarUrlAsync() ?? throw new ArgumentException($"Room {room.RoomId} does not have an avatar");
+ break;
+ }
+ case "--copy-powerlevels": {
+ var room = await GetRoomByArgument(ctx, ctx.Args[i + 1]);
+ if (room != ctx.Room) i++;
+ rb.PowerLevels = await room.GetPowerLevelsAsync() ?? throw new ArgumentException($"Room {room.RoomId} does not have power levels???");
+ break;
+ }
+ case "--invite-admin":
+ var inviteAdmin = ctx.Args[++i];
+ if (!inviteAdmin.StartsWith('@')) {
+ throw new ArgumentException("Invalid user reference: " + inviteAdmin);
+ }
+
+ rb.Invites.Add(inviteAdmin, "Marked explicitly as admin to be invited");
+ break;
+ case "--invite":
+ var inviteUser = ctx.Args[++i];
+ if (!inviteUser.StartsWith('@')) {
+ throw new ArgumentException("Invalid user reference: " + inviteUser);
+ }
+
+ rb.Invites.Add(inviteUser, "Marked explicitly to be invited");
+ break;
+ case "--name":
+ var nameEvt = rb.Name = new() { Name = "" };
+ while (i + 1 < ctx.Args.Length && !ctx.Args[i + 1].StartsWith("--")) {
+ nameEvt.Name += (nameEvt.Name.Length > 0 ? " " : "") + ctx.Args[++i];
+ }
+
+ break;
+ case "--topic":
+ var topicEvt = rb.Topic = new() { Topic = "" };
+ while (i + 1 < ctx.Args.Length && !ctx.Args[i + 1].StartsWith("--")) {
+ topicEvt.Topic += (topicEvt.Topic.Length > 0 ? " " : "") + ctx.Args[++i];
+ }
+
+ break;
+ case "--federate":
+ rb.IsFederatable = bool.Parse(ctx.Args[++i]);
+ break;
+ case "--public":
+ case "--invite-only":
+ case "--knock":
+ case "--restricted":
+ case "--knock_restricted":
+ case "--private":
+ rb.JoinRules.JoinRule = ctx.Args[i].Replace("--", "").ToLowerInvariant() switch {
+ "public" => RoomJoinRulesEventContent.JoinRules.Public,
+ "invite-only" => RoomJoinRulesEventContent.JoinRules.Invite,
+ "knock" => RoomJoinRulesEventContent.JoinRules.Knock,
+ "restricted" => RoomJoinRulesEventContent.JoinRules.Restricted,
+ "knock_restricted" => RoomJoinRulesEventContent.JoinRules.KnockRestricted,
+ "private" => RoomJoinRulesEventContent.JoinRules.Private,
+ _ => throw new ArgumentException("Unknown join rule: " + ctx.Args[i])
+ };
+ break;
+ case "--join-rule":
+ if (i + 1 >= ctx.Args.Length || !ctx.Args[i + 1].StartsWith("--")) {
+ throw new ArgumentException("Expected join rule after --join-rule");
+ }
+
+ rb.JoinRules.JoinRule = ctx.Args[++i].ToLowerInvariant() switch {
+ "public" => RoomJoinRulesEventContent.JoinRules.Public,
+ "invite" => RoomJoinRulesEventContent.JoinRules.Invite,
+ "knock" => RoomJoinRulesEventContent.JoinRules.Knock,
+ "restricted" => RoomJoinRulesEventContent.JoinRules.Restricted,
+ "knock_restricted" => RoomJoinRulesEventContent.JoinRules.KnockRestricted,
+ "private" => RoomJoinRulesEventContent.JoinRules.Private,
+ _ => throw new ArgumentException("Unknown join rule: " + ctx.Args[i])
+ };
+ break;
+ case "--history-visibility":
+ rb.HistoryVisibility = new RoomHistoryVisibilityEventContent {
+ HistoryVisibility = ctx.Args[++i].ToLowerInvariant() switch {
+ "shared" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared,
+ "invited" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Invited,
+ "joined" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Joined,
+ "world_readable" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.WorldReadable,
+ _ => throw new ArgumentException("Unknown history visibility: " + ctx.Args[i])
+ }
+ };
+ break;
+
+ default:
+ throw new ArgumentException("Unknown argument: " + ctx.Args[i]);
+ }
+ }
+
+ // await ctx.Room.SendMessageEventAsync(
+ // new MessageBuilder()
+ // .WithCodeBlock(rb.ToJson(), "json")
+ // .Build()
+ // );
+ // var result = await ctx.Homeserver.CreateRoom(creationContent);
+ var result = await rb.Create(ctx.Homeserver);
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder()
+ .WithMention($"{result.RoomId}?via={ctx.Homeserver.ServerName}", rb.CanonicalAlias.Alias)
+ .Build());
+ }
+
+ private async Task<GenericRoom> GetRoomByArgument(CommandContext ctx, string roomReference, bool defaultToCurrent = true) {
+ if (roomReference.StartsWith("--")) {
+ return defaultToCurrent ? ctx.Room : throw new ArgumentException("Invalid room reference: " + roomReference);
+ }
+
+ if (roomReference.StartsWith('!')) {
+ return ctx.Homeserver.GetRoom(roomReference);
+ }
+
+ if (roomReference.StartsWith('#')) {
+ var resolvedAlias = await ctx.Homeserver.ResolveRoomAliasAsync(roomReference);
+ return ctx.Homeserver.GetRoom(resolvedAlias.RoomId);
+ }
+
+ throw new ArgumentException("Invalid room reference: " + roomReference);
+ }
+}
\ No newline at end of file
diff --git a/MiniUtils/Commands/RedactCommand.cs b/MiniUtils/Commands/RedactCommand.cs
index e84191e..8aa65fc 100644
--- a/MiniUtils/Commands/RedactCommand.cs
+++ b/MiniUtils/Commands/RedactCommand.cs
@@ -1,6 +1,7 @@
using System.Collections.Frozen;
using ArcaneLibs.Extensions;
using LibMatrix;
+using LibMatrix.EventTypes.Spec;
using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Filters;
using LibMatrix.Helpers;
@@ -30,55 +31,83 @@ public class RedactCommand(IgnoreListManager ignoreListManager) : ICommand {
}
private async Task RedactUsers(CommandContext ctx, FrozenSet<string> senders) {
- var filter = new SyncFilter.EventFilter(senders: senders.ToList(), notTypes: ["m.room.redaction"]);
await ignoreListManager.MoveList(false, senders);
var count = 0;
- List<Task> tasks = [];
- await foreach (var resp in ctx.Room.GetManyMessagesAsync(filter: filter.ToJson(false, ignoreNull: true), chunkSize: 1000)) {
- foreach (var chunk in resp.Chunk.Chunk(49)) {
- foreach (var evt in chunk) {
- if (!senders.Contains(evt.Sender!)) continue;
- if(!await IsRedactionNeeded(ctx.Room, evt.EventId!, evt)) continue;
- tasks.Add(RedactEvent(ctx.Room, evt.EventId!));
- count++;
- }
-
- if (tasks.Count > 0) {
- await ctx.Room.SendMessageEventAsync(new MessageBuilder()
- .WithBody(
- $"[{Emojis.Hourglass}] {Emojis.Recycle} {count} ({Emojis.Checkmark} {tasks.Count(t => t.IsCompletedSuccessfully)} {Emojis.Prohibited} {tasks.Count(t => t.IsFaulted)} {Emojis.Hourglass} {tasks.Count(t => t.Status == TaskStatus.Running)})")
- .Build());
- // await Task.WhenAll(tasks);
+ // var subCount = 0;
+ // List<Task> tasks = [];
+ foreach (var senderChunk in senders.Chunk(10)) {
+ var filter = new SyncFilter.EventFilter(senders: senderChunk.ToList(), notTypes: ["m.room.redaction"]);
+
+ await foreach (var resp in ctx.Room.GetManyMessagesAsync(filter: filter.ToJson(false, ignoreNull: true), chunkSize: 1000)) {
+ // foreach (var chunk in resp.Chunk.Chunk(49)) {
+ // foreach (var evt in chunk) {
+ // if (!senders.Contains(evt.Sender!)) continue;
+ // if (!await IsRedactionNeeded(ctx.Room, evt.EventId!, evt)) continue;
+ // tasks.Add(RedactEvent(ctx.Room, evt.EventId!));
+ // count++;
+ // subCount++;
+ // }
+ //
+ // if (subCount >= 40) {
+ // await ctx.Room.SendMessageEventAsync(new MessageBuilder()
+ // .WithBody(
+ // $"[{Emojis.Hourglass}] {Emojis.Recycle} {count} ({Emojis.Checkmark} {tasks.Count(t => t.IsCompletedSuccessfully)} {Emojis.Prohibited} {tasks.Count(t => t.IsFaulted)} {Emojis.Hourglass} {tasks.Count(t => t.Status == TaskStatus.Running)})")
+ // .Build());
+ // // await Task.WhenAll(tasks);
+ // subCount = 0;
+ // }
+ // }
+ var toRedactQueryTask = resp.Chunk
+ .Where(x => senders.Contains(x.Sender!))
+ .Select(async x => (x, await IsRedactionNeeded(ctx.Room, x.EventId!, x)))
+ .ToList();
+ var toRedact = (await Task.WhenAll(toRedactQueryTask)).Where(x => x.Item2).Select(x => x.x).ToList();
+ foreach (var chunk in toRedact.Chunk(49)) {
+ var toSend = chunk.Select(x => new StateEvent() {
+ Type = RoomRedactionEventContent.EventId,
+ TypedContent = new RoomRedactionEventContent() {
+ Redacts = x.EventId
+ }
+ }).ToList();
+ toSend.Add(new StateEvent() {
+ Type = RoomMessageEventContent.EventId,
+ TypedContent =
+ new MessageBuilder()
+ .WithBody(
+ $"[{Emojis.Hourglass}] {Emojis.Recycle} {count} ({Emojis.Checkmark} {count} {Emojis.Hourglass} {toSend.Count})")
+ .Build()
+ });
+ count += toSend.Count - 1;
+ await ctx.Room.BulkSendEventsAsync(toSend);
}
}
}
- await Task.WhenAll(tasks);
-
+ // await Task.WhenAll(tasks);
+
await ctx.Room.SendMessageEventAsync(new MessageBuilder().WithBody($"{Emojis.Recycle} {count}").Build());
// await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.Recycle} {count}");
}
private async Task<bool> IsRedactionNeeded(GenericRoom roomId, string eventId, StateEventResponse? evt = null) {
evt ??= await roomId.GetEventAsync(eventId);
-
+
// Ignore room member state events
if (evt is { StateKey: not null, Type: not RoomMemberEventContent.EventId }) return false;
-
+
// Ignore redaction events
if (evt is { Type: RoomRedactionEventContent.EventId }) return false;
-
+
// Ignore empty events
if (evt is { RawContent: null or { Count: 0 } }) return false;
-
+
// Ignore redacted events
if (evt.Unsigned?.ContainsKey("redacted_because") == true) return false;
-
-
- throw new NotImplementedException("Redaction check not implemented");
+ return true;
+ // throw new NotImplementedException("Redaction check not implemented");
}
-
+
private async Task RedactEvent(GenericRoom room, string eventId) {
bool success;
do {
diff --git a/MiniUtils/Commands/ServersCommand.cs b/MiniUtils/Commands/ServersCommand.cs
new file mode 100644
index 0000000..7e5c97a
--- /dev/null
+++ b/MiniUtils/Commands/ServersCommand.cs
@@ -0,0 +1,79 @@
+using System.Collections.Frozen;
+using ArcaneLibs.Extensions;
+using LibMatrix;
+using LibMatrix.EventTypes.Spec;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Filters;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses.Federation;
+using LibMatrix.RoomTypes;
+using LibMatrix.Services;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MiniUtils.Classes;
+using MiniUtils.Services;
+
+namespace MiniUtils.Commands;
+
+public class ServersCommand(HomeserverProviderService hsProvider) : ICommand {
+ public string Name => "servers";
+
+ public string[]? Aliases => [];
+
+ public string Description => "Get a list of servers in the room";
+
+ public bool Unlisted => false;
+
+ public async Task Invoke(CommandContext ctx) {
+ var lastUpdated = DateTime.Now;
+ var servers = (await ctx.Room.GetMembersByHomeserverAsync()).Keys.Select(GetServerVersionAsync).ToList().ToAsyncResultEnumerable();
+ Dictionary<object, List<string>> serverVersions = new();
+
+ var message = new MessageBuilder()
+ .WithBody($"[{Emojis.Hourglass}] {Emojis.Recycle} Gathering server versions, please wait...")
+ .Build();
+ var eventId = await ctx.Room.SendMessageEventAsync(message);
+
+ await foreach (var result in servers) {
+ if (!serverVersions.TryGetValue(result, out var serverNames)) {
+ serverNames = [];
+ serverVersions[result] = serverNames;
+ }
+
+ serverNames.Add(result.Server);
+
+ if (DateTime.Now - lastUpdated > TimeSpan.FromMilliseconds(500)) {
+ lastUpdated = DateTime.Now;
+ var msb = new MessageBuilder()
+ .WithBody($"[{Emojis.Hourglass}] {serverVersions.Count} servers found so far:").WithNewline();
+
+ foreach (var (res, serversByVersion) in serverVersions.Where(x => x.Key is ServerVersionResponse)) {
+ var svr = (ServerVersionResponse)res;
+ msb.WithBody($"- {svr.Server.Name} {svr.Server.Version}: {string.Join(", ", serversByVersion)}").WithNewline();
+ }
+
+ await ctx.Room.SendMessageEventAsync(
+ msb.Build()
+ .SetReplaceRelation<RoomMessageEventContent>(eventId.EventId)
+ );
+ }
+ }
+
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder()
+ .WithBody($"[{Emojis.Bullseye}] {serverVersions.Count} servers found:")
+ .Build()
+ .SetReplaceRelation<RoomMessageEventContent>(eventId.EventId)
+ );
+ }
+
+ private async Task<(string Server, object Result)> GetServerVersionAsync(string server) {
+ try {
+ var hs = await hsProvider.GetRemoteHomeserver(server);
+ var version = await hs.FederationClient.GetServerVersionAsync();
+ return (server, version);
+ }
+ catch (Exception e) {
+ return (server, e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/MiniUtils/Commands/SpamCommand.cs b/MiniUtils/Commands/SpamCommand.cs
index 742ae3b..01f7244 100644
--- a/MiniUtils/Commands/SpamCommand.cs
+++ b/MiniUtils/Commands/SpamCommand.cs
@@ -1,3 +1,8 @@
+using System.Net.Http.Json;
+using LibMatrix;
+using LibMatrix.EventTypes.Spec.State.Policy;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Extensions;
using LibMatrix.Helpers;
using LibMatrix.RoomTypes;
using LibMatrix.Utilities.Bot.Interfaces;
@@ -16,11 +21,62 @@ public class SpamCommand(IgnoreListManager ignoreListManager) : ICommand {
public bool Unlisted => true;
public async Task Invoke(CommandContext ctx) {
- var tasks = Enumerable.Range(0, 10000)
- .Select(i => SendMessage(ctx.Room, i.ToString()))
- .ToList();
- await Task.WhenAll(tasks);
- await ctx.Room.SendMessageEventAsync(new MessageBuilder().WithBody($"{Emojis.Recycle}").Build());
+ // var tasks = Enumerable.Range(0, 10000)
+ // .Select(i => SendMessage(ctx.Room, i.ToString()))
+ // .ToList();
+ // await Task.WhenAll(tasks);
+ // await ctx.Room.SendMessageEventAsync(new MessageBuilder().WithBody($"{Emojis.Recycle}").Build());
+ //
+ for (int i = 0; i < 8; i++) {
+ // _ = ctx.Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/unstable/gay.rory.bulk_send_events/rooms/{ctx.Room.RoomId}/bulk_send_events",
+ // CreateMessagesAsync());
+
+ // await new MatrixHttpClient(){BaseAddress = new("http://127.0.0.1:8888")}.GetAsync($"/_matrix/client/unstable/gay.rory.bulk_send_events/rooms/{ctx.Room.RoomId}/bulk_send_events");
+ _ = ctx.Homeserver.ClientHttpClient
+ // _ = new MatrixHttpClient(){BaseAddress = new("http://127.0.0.1:8888")}
+ .PostAsyncEnumerableAsJsonAsync($"/_matrix/client/unstable/gay.rory.bulk_send_events/rooms/{ctx.Room.RoomId}/bulk_send_events?_r={Guid.NewGuid()}",
+ CreateMessagesAsync(ctx.Room));
+ }
+ }
+
+ private async IAsyncEnumerable<StateEvent> CreateMessagesAsync(GenericRoom room) {
+ int i = 0;
+ // var pls = await room.GetStateEventAsync(RoomPowerLevelEventContent.EventId);
+ // var pls2 = await room.GetStateEventAsync(RoomPowerLevelEventContent.EventId);
+ // if (pls2.TypedContent is RoomPowerLevelEventContent pl2) {
+ // pl2.Ban = 5;
+ // pl2.Users!["@emma:synapse.localhost"] = 102;
+ // pls2.TypedContent = pl2;
+ // }
+ //
+ // yield return new() {
+ // RawContent = pls2.RawContent,
+ // Type = RoomPowerLevelEventContent.EventId,
+ // StateKey = ""
+ // // StateKey = Guid.NewGuid().ToString()
+ // };
+ while (i++ < 200) {
+ // await Task.Delay(500);
+ Console.WriteLine(i);
+ // yield return new() {
+ // Type = "m.room.message",
+ // TypedContent = new MessageBuilder().WithBody(i.ToString()).Build()
+ // };
+
+ // yield return new() {
+ // RawContent = pls.RawContent,
+ // Type = RoomPowerLevelEventContent.EventId,
+ // StateKey = ""
+ // // StateKey = Guid.NewGuid().ToString()
+ // };
+ yield return new() {
+ TypedContent = new UserPolicyRuleEventContent() {
+ Entity = $"@{Guid.NewGuid()}:{room.Homeserver.ServerName}",
+ },
+ Type = UserPolicyRuleEventContent.EventId,
+ StateKey = Guid.NewGuid().ToString()
+ };
+ }
}
private async Task SendMessage(GenericRoom room, string content) {
diff --git a/MiniUtils/MiniUtils.csproj b/MiniUtils/MiniUtils.csproj
index f7af751..5ac38cb 100644
--- a/MiniUtils/MiniUtils.csproj
+++ b/MiniUtils/MiniUtils.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
- <TargetFramework>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
@@ -14,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2"/>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
diff --git a/MiniUtils/Program.cs b/MiniUtils/Program.cs
index acf1902..612b4ad 100644
--- a/MiniUtils/Program.cs
+++ b/MiniUtils/Program.cs
@@ -1,7 +1,9 @@
using LibMatrix.Extensions;
+using LibMatrix.Helpers;
using LibMatrix.Services;
using LibMatrix.Utilities.Bot;
using MiniUtils;
+using MiniUtils.Classes;
using MiniUtils.Core;
using MiniUtils.Services;
using MiniUtils.Utilities;
@@ -11,7 +13,16 @@ var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddRoryLibMatrixServices()
.AddMatrixBot()
.AddCommandHandler()
- .DiscoverAllCommands();
+ .DiscoverAllCommands()
+ .WithCommandResultHandler(async result => {
+ if(result.Exception is not null)
+ await result.Context.Room.SendMessageEventAsync(
+ new MessageBuilder()
+ .WithBody($"[{Emojis.Prohibited}] An error occurred while processing your command: {result.Exception.Message}")
+ .WithNewline().WithCodeBlock(result.Exception.ToString(), "csharp")
+ .Build()
+ );
+ });
builder.Services.AddSingleton<MiniUtilsConfiguration>();
builder.Services.AddSingleton<MscInfoProvider>();
@@ -20,6 +31,8 @@ builder.Services.AddSingleton<IgnoreListManager>();
builder.Services.AddHostedService<MiniUtilsWorker>();
builder.Services.AddHostedService<AutoTombstoneFollowerService>();
+
+
// builder.Services.AddSingleton<PolicyStore>();
// MatrixHttpClient.LogRequests = false;
diff --git a/MiniUtils/Services/IgnoreListManager.cs b/MiniUtils/Services/IgnoreListManager.cs
index 3b6dc96..cb863a8 100644
--- a/MiniUtils/Services/IgnoreListManager.cs
+++ b/MiniUtils/Services/IgnoreListManager.cs
@@ -65,7 +65,7 @@ public class IgnoreListManager(AuthenticatedHomeserverGeneric homeserver) {
return moved;
}
- public async Task<int> AddList(string[] itemsToAdd) {
+ public async Task<int> AddList(IEnumerable<string> itemsToAdd) {
int added = 0;
await Lock.WaitAsync();
var ignoreList = await homeserver.GetAccountDataOrNullAsync<IgnoredUserListEventContentWithDisabled>(IgnoredUserListEventContent.EventId) ?? new();
@@ -76,12 +76,30 @@ public class IgnoreListManager(AuthenticatedHomeserverGeneric homeserver) {
added++;
continue;
}
+
ignoreList.IgnoredUsers.Add(item, new());
added++;
}
+
if (added > 0)
await homeserver.SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoreList);
Lock.Release();
return added;
}
+
+ public async Task<int> RemoveList(IEnumerable<string> itemsToRemove) {
+ int removed = 0;
+ await Lock.WaitAsync();
+ var ignoreList = await homeserver.GetAccountDataOrNullAsync<IgnoredUserListEventContentWithDisabled>(IgnoredUserListEventContent.EventId) ?? new();
+ foreach (var item in itemsToRemove) {
+ if (ignoreList.IgnoredUsers.Remove(item)) removed++;
+ if (ignoreList.DisabledIgnoredUsers.Remove(item)) removed++;
+ removed++;
+ }
+
+ if (removed > 0)
+ await homeserver.SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoreList);
+ Lock.Release();
+ return removed;
+ }
}
\ No newline at end of file
diff --git a/MiniUtils/appsettings.Development.json.h b/MiniUtils/appsettings.Development.json.h
new file mode 100644
index 0000000..4cfb975
--- /dev/null
+++ b/MiniUtils/appsettings.Development.json.h
@@ -0,0 +1,53 @@
+{
+ // Don't touch this unless you know what you're doing:
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "LibMatrixBot": {
+ // Homeserver to connect to.
+ // Note: Homeserver resolution is applied here, but a direct base URL can be used.
+ "Homeserver": "rory.gay",
+
+ // Absolute path to the file containing the access token
+ "AccessTokenPath": "/home/Rory/matrix_access_token",
+ "InviteHandler": {
+ "SyncConfiguration": {
+ // How long to wait until the sync request times out
+ // "Timeout": 300000,
+
+ // Minimum sync interval, useful if you want to have less traffic than a normal client.
+ "MinimumSyncTime": "00:00:10.000",
+
+ // What presence value to set
+ // Defaults to "online" if null or not set
+ // "Presence": "online",
+
+ // Filter to apply to the sync request. Useful if you want custom data to be sent.
+ // "Filter": { },
+
+ // Whether to initial sync on startup - very useful in development, or just to be sure.
+ "InitialSyncOnStartup": true
+ }
+ }
+ },
+ "AntiDmSpam": {
+ // Whether invites should be logged to a room.
+ "LogRoom": "!GrLSwdAkdrvfMrRYKR:rory.gay",
+ "LogInviteDataAsFile": true,
+ // Whether to report users and rooms when an invite is blocked.
+ "ReportBlockedInvites": true,
+ // WARNING: If you're a room moderator, this will cause your client to not receive events from ignored users!
+ "IgnoreBannedUsers": true,
+ // Policy lists to follow
+ "PolicyLists": [
+ {
+ "Name": "Community Moderation Effort",
+ "RoomId": "!fTjMjIzNKEsFlUIiru:neko.dev",
+ "Vias": [ "rory.gay" ]
+ }
+ ]
+ }
+}
\ No newline at end of file
|