diff --git a/.gitignore b/.gitignore
index 984ec54..67e71e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ MatrixRoomUtils.Bot/bot_data/
appsettings.Local*.json
nixpkgs/
*.DotSettings.user
+**/.DS_Store
*.patch
test.tsv
diff --git a/.gitmodules b/.gitmodules
index 8cbedc0..487c63b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,6 @@
[submodule "LibMatrix"]
path = LibMatrix
- url = https://git.rory.gay/matrix/LibMatrix.git
+ url = https://cgit.rory.gay/matrix/LibMatrix.git
[submodule "MxApiExtensions"]
path = MxApiExtensions
url = https://cgit.rory.gay/matrix/tools/MxApiExtensions.git
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 55540ea..0000000
--- a/.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<component name="InspectionProjectProfileManager">
- <profile version="1.0">
- <option name="myName" value="Project Default" />
- <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
- <option name="myValues">
- <value>
- <list size="7">
- <item index="0" class="java.lang.String" itemvalue="nobr" />
- <item index="1" class="java.lang.String" itemvalue="noembed" />
- <item index="2" class="java.lang.String" itemvalue="comment" />
- <item index="3" class="java.lang.String" itemvalue="noscript" />
- <item index="4" class="java.lang.String" itemvalue="embed" />
- <item index="5" class="java.lang.String" itemvalue="script" />
- <item index="6" class="java.lang.String" itemvalue="width" />
- </list>
- </value>
- </option>
- <option name="myCustomValuesEnabled" value="true" />
- </inspection_tool>
- <inspection_tool class="JsonStandardCompliance" enabled="false" level="ERROR" enabled_by_default="false" />
- </profile>
-</component>
\ No newline at end of file
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/.gitignore b/.idea/.idea.MatrixUtils/.idea/.gitignore
index f938d5a..f938d5a 100644
--- a/.idea/.idea.MatrixRoomUtils/.idea/.gitignore
+++ b/.idea/.idea.MatrixUtils/.idea/.gitignore
diff --git a/.idea/.idea.MatrixUtils/.idea/.name b/.idea/.idea.MatrixUtils/.idea/.name
new file mode 100644
index 0000000..65d8b74
--- /dev/null
+++ b/.idea/.idea.MatrixUtils/.idea/.name
@@ -0,0 +1 @@
+MatrixUtils
\ No newline at end of file
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml b/.idea/.idea.MatrixUtils/.idea/avalonia.xml
index 0aa65bb..0aa65bb 100644
--- a/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml
+++ b/.idea/.idea.MatrixUtils/.idea/avalonia.xml
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/codeStyles/codeStyleConfig.xml b/.idea/.idea.MatrixUtils/.idea/codeStyles/codeStyleConfig.xml
index a55e7a1..a55e7a1 100644
--- a/.idea/.idea.MatrixRoomUtils/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/.idea.MatrixUtils/.idea/codeStyles/codeStyleConfig.xml
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/encodings.xml b/.idea/.idea.MatrixUtils/.idea/encodings.xml
index df87cf9..df87cf9 100644
--- a/.idea/.idea.MatrixRoomUtils/.idea/encodings.xml
+++ b/.idea/.idea.MatrixUtils/.idea/encodings.xml
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml b/.idea/.idea.MatrixUtils/.idea/indexLayout.xml
index 4520708..d166ec4 100644
--- a/.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml
+++ b/.idea/.idea.MatrixUtils/.idea/indexLayout.xml
@@ -2,6 +2,7 @@
<project version="4">
<component name="UserContentModel">
<attachedFolders>
+ <Path>LibMatrix/LibMatrix/Homeservers</Path>
<Path>MatrixRoomUtils.Bot/bot_data</Path>
<Path>MatrixRoomUtils.Desktop/bin/Debug/net7.0/mru-desktop</Path>
</attachedFolders>
diff --git a/.idea/.idea.MatrixUtils/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.MatrixUtils/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..0e61b0a
--- /dev/null
+++ b/.idea/.idea.MatrixUtils/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,53 @@
+<component name="InspectionProjectProfileManager">
+ <profile version="1.0">
+ <option name="myName" value="Project Default" />
+ <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="myValues">
+ <value>
+ <list size="7">
+ <item index="0" class="java.lang.String" itemvalue="nobr" />
+ <item index="1" class="java.lang.String" itemvalue="noembed" />
+ <item index="2" class="java.lang.String" itemvalue="comment" />
+ <item index="3" class="java.lang.String" itemvalue="noscript" />
+ <item index="4" class="java.lang.String" itemvalue="embed" />
+ <item index="5" class="java.lang.String" itemvalue="script" />
+ <item index="6" class="java.lang.String" itemvalue="width" />
+ </list>
+ </value>
+ </option>
+ <option name="myCustomValuesEnabled" value="true" />
+ </inspection_tool>
+ <inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+ <option name="ignoredUrls">
+ <list>
+ <option value="http://" />
+ <option value="http://0.0.0.0" />
+ <option value="http://127.0.0.1" />
+ <option value="http://activemq.apache.org/schema/" />
+ <option value="http://cxf.apache.org/schemas/" />
+ <option value="http://java.sun.com/" />
+ <option value="http://javafx.com/fxml" />
+ <option value="http://javafx.com/javafx/" />
+ <option value="http://json-schema.org/draft" />
+ <option value="http://localhost" />
+ <option value="http://maven.apache.org/POM/" />
+ <option value="http://maven.apache.org/xsd/" />
+ <option value="http://primefaces.org/ui" />
+ <option value="http://schema.cloudfoundry.org/spring/" />
+ <option value="http://schemas.xmlsoap.org/" />
+ <option value="http://tiles.apache.org/" />
+ <option value="http://www.ibm.com/webservices/xsd" />
+ <option value="http://www.jboss.com/xml/ns/" />
+ <option value="http://www.jboss.org/j2ee/schema/" />
+ <option value="http://www.springframework.org/schema/" />
+ <option value="http://www.springframework.org/security/tags" />
+ <option value="http://www.springframework.org/tags" />
+ <option value="http://www.thymeleaf.org" />
+ <option value="http://www.w3.org/" />
+ <option value="http://xmlns.jcp.org/" />
+ </list>
+ </option>
+ </inspection_tool>
+ <inspection_tool class="JsonStandardCompliance" enabled="false" level="ERROR" enabled_by_default="false" />
+ </profile>
+</component>
\ No newline at end of file
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/vcs.xml b/.idea/.idea.MatrixUtils/.idea/vcs.xml
index 94a25f7..94a25f7 100644
--- a/.idea/.idea.MatrixRoomUtils/.idea/vcs.xml
+++ b/.idea/.idea.MatrixUtils/.idea/vcs.xml
diff --git a/Benchmarks/.gitignore b/Benchmarks/.gitignore
new file mode 100644
index 0000000..a7d52bf
--- /dev/null
+++ b/Benchmarks/.gitignore
@@ -0,0 +1,3 @@
+
+BenchmarkDotNet.Artifacts
+benchmark.log
diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj
new file mode 100644
index 0000000..5d584c9
--- /dev/null
+++ b/Benchmarks/Benchmarks.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net9.0</TargetFramework>
+ <LangVersion>preview</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+<!-- <PublishAot>true</PublishAot>-->
+ <InvariantGlobalization>true</InvariantGlobalization>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
+ </ItemGroup>
+
+</Project>
diff --git a/Benchmarks/Program.cs b/Benchmarks/Program.cs
new file mode 100644
index 0000000..90d004a
--- /dev/null
+++ b/Benchmarks/Program.cs
@@ -0,0 +1,303 @@
+// See https://aka.ms/new-console-template for more information
+
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Running;
+
+[SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 5, iterationCount: 5, id: "FastAndDirtyJob")]
+[InProcess]
+[ProcessCount(4)]
+public class Program {
+ public static void Main(string[] args) {
+ BenchmarkRunner.Run<Program>(args: args);
+ }
+
+ [Params(true, false)]
+ public bool DoDisambiguate { get; set; } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateProfileUpdates {
+ get => field && DoDisambiguate;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateKicks {
+ get => field && DoDisambiguate;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateUnbans {
+ get => field && DoDisambiguate;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateInviteAccepted {
+ get => field && DoDisambiguate && DisambiguateInviteActions;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateInviteRejected {
+ get => field && DoDisambiguate && DisambiguateInviteActions;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateInviteRetracted {
+ get => field && DoDisambiguate && DisambiguateInviteActions;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateKnockAccepted {
+ get => field && DoDisambiguate && DisambiguateKnockActions;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateKnockRejected {
+ get => field && DoDisambiguate && DisambiguateKnockActions;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateKnockRetracted {
+ get => field && DoDisambiguate && DisambiguateKnockActions;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateKnockActions {
+ get => field && DoDisambiguate;
+ set;
+ } = true;
+
+ [Params(true, false)]
+ public bool DisambiguateInviteActions {
+ get => field && DoDisambiguate;
+ set;
+ } = true;
+
+ public enum MembershipTransition : uint {
+ None,
+ Join = 0b0001,
+ Leave = 0b0010,
+ Knock = 0b0100,
+ Invite = 0b1000,
+ Ban = 0b1001,
+
+ // disambiguated
+ ProfileUpdate = 0b0000_0001_0001,
+ Kick = 0b0000_0001_0010,
+ Unban = 0b0000_0010_0010,
+ InviteAccepted = 0b0000_0100_0001,
+ InviteRejected = 0b0000_1000_0010,
+ InviteRetracted = 0b0001_0000_0010,
+ KnockAccepted = 0b0010_0000_1000,
+ KnockRejected = 0b0100_0000_0010,
+ KnockRetracted = 0b1000_0000_0010
+ }
+
+ public readonly struct MembershipEntry {
+ public required MembershipTransition State { get; init; }
+ public string Aba { get; init; }
+ public string Abb { get; init; }
+ public string Abc { get; init; }
+ public string Abd { get; init; }
+ }
+
+ [Params(100, 10_000, 1_000_000)] public int N;
+
+ [GlobalSetup]
+ public void Setup() {
+ entries = Enumerable.Range(0, N).Select(_ => new MembershipEntry() {
+ State = (MembershipTransition)new Random().Next(1, 16),
+ Aba = Guid.NewGuid().ToString(),
+ Abb = Guid.NewGuid().ToString(),
+ Abc = Guid.NewGuid().ToString(),
+ Abd = Guid.NewGuid().ToString()
+ }).ToImmutableList();
+ }
+
+ public ImmutableList<MembershipEntry> entries = ImmutableList<MembershipEntry>.Empty;
+
+ [Benchmark]
+ public void TestTruthyness() {
+ var @switch = AmbiguateMembershipsSwitch().GetEnumerator();
+ var @switchpm = AmbiguateMembershipsSwitchPatternMatching().GetEnumerator();
+ var @if = AmbiguateMembershipsIf().GetEnumerator();
+ var @map = AmbiguateMembershipsStaticMap().GetEnumerator();
+ var @binmask = AmbiguateMembershipsBinMask().GetEnumerator();
+
+ while (@switch.MoveNext() && @map.MoveNext() && @if.MoveNext() && @switchpm.MoveNext() && @binmask.MoveNext()) {
+ if (@switch.Current.State != @map.Current.State || @switch.Current.State != @if.Current.State || @switch.Current.State != @switchpm.Current.State ||
+ @switch.Current.State != @binmask.Current.State) {
+ throw new InvalidOperationException("Results do not match!");
+ }
+ }
+
+ @switch.Dispose();
+ @switchpm.Dispose();
+ @if.Dispose();
+ @map.Dispose();
+ @binmask.Dispose();
+ }
+
+ [Benchmark]
+ public void TestAmbiguateMembershipsSwitchPatternMatching() => AmbiguateMembershipsSwitchPatternMatching().Consume(new Consumer());
+
+ public IEnumerable<MembershipEntry> AmbiguateMembershipsSwitchPatternMatching() {
+ foreach (var entry in entries) {
+ var newState = entry.State switch {
+ MembershipTransition.ProfileUpdate when !DoDisambiguate || !DisambiguateProfileUpdates => MembershipTransition.Join,
+ MembershipTransition.Kick when !DoDisambiguate || !DisambiguateKicks => MembershipTransition.Leave,
+ MembershipTransition.Unban when !DoDisambiguate || !DisambiguateUnbans => MembershipTransition.Leave,
+ MembershipTransition.InviteAccepted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted => MembershipTransition.Join,
+ MembershipTransition.InviteRejected when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected => MembershipTransition.Leave,
+ MembershipTransition.InviteRetracted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted => MembershipTransition.Leave,
+ MembershipTransition.KnockAccepted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted => MembershipTransition.Invite,
+ MembershipTransition.KnockRejected when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected => MembershipTransition.Leave,
+ MembershipTransition.KnockRetracted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted => MembershipTransition.Leave,
+ _ => entry.State
+ };
+ yield return newState == entry.State ? entry : entry with { State = newState };
+ }
+ }
+
+ [Benchmark]
+ public void TestAmbiguateMembershipsSwitch() => AmbiguateMembershipsSwitch().Consume(new Consumer());
+
+ public IEnumerable<MembershipEntry> AmbiguateMembershipsSwitch() {
+ foreach (var entry in entries) {
+ if (!DoDisambiguate) {
+ yield return entry;
+ continue;
+ }
+
+ MembershipTransition newState;
+ switch (entry.State) {
+ case MembershipTransition.ProfileUpdate:
+ newState = !DisambiguateProfileUpdates ? MembershipTransition.Join : entry.State;
+ break;
+ case MembershipTransition.Kick:
+ newState = !DisambiguateKicks ? MembershipTransition.Leave : entry.State;
+ break;
+ case MembershipTransition.Unban when !DisambiguateUnbans:
+ newState = MembershipTransition.Leave;
+ break;
+ case MembershipTransition.InviteAccepted when !DisambiguateInviteActions || !DisambiguateInviteAccepted:
+ newState = MembershipTransition.Join;
+ break;
+ case MembershipTransition.InviteRejected when !DisambiguateInviteActions || !DisambiguateInviteRejected:
+
+ newState = MembershipTransition.Leave;
+ break;
+ case MembershipTransition.InviteRetracted when !DisambiguateInviteActions || !DisambiguateInviteRetracted:
+ newState = MembershipTransition.Leave;
+ break;
+ case MembershipTransition.KnockAccepted when !DisambiguateKnockActions || !DisambiguateKnockAccepted:
+ newState = MembershipTransition.Invite;
+ break;
+ case MembershipTransition.KnockRejected when !DisambiguateKnockActions || !DisambiguateKnockRejected:
+ newState = MembershipTransition.Leave;
+ break;
+ case MembershipTransition.KnockRetracted when !DisambiguateKnockActions || !DisambiguateKnockRetracted:
+ newState = MembershipTransition.Leave;
+ break;
+ default:
+ newState = entry.State;
+ break;
+ }
+
+ yield return newState == entry.State ? entry : entry with { State = newState };
+ }
+ }
+
+ [Benchmark]
+ public void TestAmbiguateMembershipsIf() => AmbiguateMembershipsIf().Consume(new Consumer());
+
+ public IEnumerable<MembershipEntry> AmbiguateMembershipsIf() {
+ foreach (var entry in entries) {
+ MembershipTransition newState;
+ if (entry.State == MembershipTransition.ProfileUpdate && (!DoDisambiguate || !DisambiguateProfileUpdates))
+ newState = MembershipTransition.Join;
+ else if ((entry.State == MembershipTransition.Kick && (!DoDisambiguate || !DisambiguateKicks)) ||
+ (entry.State == MembershipTransition.Unban && (!DoDisambiguate || !DisambiguateUnbans)))
+ newState = MembershipTransition.Leave;
+ else if (entry.State == MembershipTransition.InviteAccepted && (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted))
+ newState = MembershipTransition.Join;
+ else if ((entry.State == MembershipTransition.InviteRejected && (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected)) ||
+ (entry.State == MembershipTransition.InviteRetracted && (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted)))
+ newState = MembershipTransition.Leave;
+ else if (entry.State == MembershipTransition.KnockAccepted && (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted))
+ newState = MembershipTransition.Invite;
+ else if ((entry.State == MembershipTransition.KnockRejected && (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected)) ||
+ (entry.State == MembershipTransition.KnockRetracted && (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted)))
+ newState = MembershipTransition.Leave;
+ else
+ newState = entry.State;
+
+ yield return newState == entry.State ? entry : entry with { State = newState };
+ }
+ }
+
+ [Benchmark]
+ public void TestAmbiguateMembershipsStaticMap() => AmbiguateMembershipsStaticMap().Consume(new Consumer());
+
+ public IEnumerable<MembershipEntry> AmbiguateMembershipsStaticMap() {
+ Dictionary<MembershipTransition, MembershipTransition> _map = [];
+ if (!DoDisambiguate || !DisambiguateProfileUpdates) _map[MembershipTransition.ProfileUpdate] = MembershipTransition.Join;
+ if (!DoDisambiguate || !DisambiguateKicks) _map[MembershipTransition.Kick] = MembershipTransition.Leave;
+ if (!DoDisambiguate || !DisambiguateUnbans) _map[MembershipTransition.Unban] = MembershipTransition.Leave;
+ if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted) _map[MembershipTransition.InviteAccepted] = MembershipTransition.Join;
+ if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected) _map[MembershipTransition.InviteRejected] = MembershipTransition.Leave;
+ if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted) _map[MembershipTransition.InviteRetracted] = MembershipTransition.Leave;
+ if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted) _map[MembershipTransition.KnockAccepted] = MembershipTransition.Invite;
+ if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected) _map[MembershipTransition.KnockRejected] = MembershipTransition.Leave;
+ if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted) _map[MembershipTransition.KnockRetracted] = MembershipTransition.Leave;
+ FrozenDictionary<MembershipTransition, MembershipTransition> map = _map.ToFrozenDictionary();
+ // _map
+ foreach (var entry in entries) {
+ var newState = map.TryGetValue(entry.State, out var value) ? value : entry.State;
+ yield return newState == entry.State ? entry : entry with { State = newState };
+ }
+ }
+
+ [Benchmark]
+ public void TestAmbiguateMembershipsBinMask() => AmbiguateMembershipsBinMask().Consume(new Consumer());
+
+ public IEnumerable<MembershipEntry> AmbiguateMembershipsBinMask() {
+ uint mask = 0;
+ // dont mask last 4 bits
+ if (!DoDisambiguate || !DisambiguateProfileUpdates) mask |= (uint)MembershipTransition.ProfileUpdate >> 4;
+ if (!DoDisambiguate || !DisambiguateKicks) mask |= (uint)MembershipTransition.Kick >> 4;
+ if (!DoDisambiguate || !DisambiguateUnbans) mask |= (uint)MembershipTransition.Unban >> 4;
+ if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted) mask |= (uint)MembershipTransition.InviteAccepted >> 4;
+ if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected) mask |= (uint)MembershipTransition.InviteRejected >> 4;
+ if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted) mask |= (uint)MembershipTransition.InviteRetracted >> 4;
+ if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted) mask |= (uint)MembershipTransition.KnockAccepted >> 4;
+ if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected) mask |= (uint)MembershipTransition.KnockRejected >> 4;
+ if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted) mask |= (uint)MembershipTransition.KnockRetracted >> 4;
+ mask = (mask << 4) + 0b1111;
+ // Console.WriteLine(mask.ToString("b24"));
+ foreach (var entry in entries) {
+ if (((uint)entry.State & 0b1111_1111_0000) == 0) {
+ yield return entry;
+ continue;
+ }
+
+ var newState = (MembershipTransition)((uint)entry.State & mask);
+ // Console.WriteLine(((uint)newState).ToString("b32"));
+ yield return newState == entry.State ? entry : entry with { State = newState };
+ }
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix b/LibMatrix
-Subproject 16e314ed714f8b3e298c0ecf2ebfe67b48e5f69
+Subproject cacabe2b1a15bb7492e23d477ec653513e84d26
diff --git a/MatrixRoomUtils.sln b/MatrixRoomUtils.sln
deleted file mode 100644
index 23d86b0..0000000
--- a/MatrixRoomUtils.sln
+++ /dev/null
@@ -1,232 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-#
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Web", "MatrixUtils.Web\MatrixUtils.Web.csproj", "{D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Web.Server", "MatrixUtils.Web.Server\MatrixUtils.Web.Server.csproj", "{F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Desktop", "MatrixUtils.Desktop\MatrixUtils.Desktop.csproj", "{27C08A4F-5AF0-4C2C-AFCB-050E3388C116}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LibMatrix", "LibMatrix", "{8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix\LibMatrix.csproj", "{F4E241C3-0300-4B87-8707-BCBDEF1F0185}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MxApiExtensions", "MxApiExtensions", "{F1376F9A-FB65-4E60-BB9A-62A64F741FF4}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions", "MxApiExtensions\MxApiExtensions\MxApiExtensions.csproj", "{41200A7B-D2DB-4656-B66B-5206A63B367A}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "LibMatrix\ArcaneLibs", "{B00C5CB6-6200-4B41-96BE-C6EAF1085A14}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "LibMatrix\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{2D6F31D7-3139-44EC-9D11-486282DD4ED1}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions.Classes", "MxApiExtensions\MxApiExtensions.Classes\MxApiExtensions.Classes.csproj", "{99C016AA-AFBA-4D32-A687-D1FABC0F5212}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions.Classes.LibMatrix", "MxApiExtensions\MxApiExtensions.Classes.LibMatrix\MxApiExtensions.Classes.LibMatrix.csproj", "{C298E274-5D6C-47C8-9B71-A6B34D0195A3}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExampleBots", "ExampleBots", "{3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.ExampleBot", "LibMatrix\ExampleBots\LibMatrix.ExampleBot\LibMatrix.ExampleBot.csproj", "{2CB12623-4918-4176-9B4A-88D846CCD3ED}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModerationBot", "LibMatrix\ExampleBots\ModerationBot\ModerationBot.csproj", "{48DBB05F-B007-4B24-89B3-3CC177C79007}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "LibMatrix\Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{5DEF66C8-B931-435F-B4B5-E1858590D52E}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{D8E5C678-3BE5-470C-A3A5-B5D525FC2012}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralContactBotPoC", "LibMatrix\ExampleBots\PluralContactBotPoC\PluralContactBotPoC.csproj", "{95052EE6-7513-46FB-91BD-EE82026B42F1}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{7D2C9959-8309-4110-A67F-DEE64E97C1D8}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "LibMatrix\Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestDataGenerator", "LibMatrix\Tests\TestDataGenerator\TestDataGenerator.csproj", "{F3312DE9-4335-4E85-A4CF-2616427A651E}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.LibDMSpace", "MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj", "{EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{1CAA2B6D-0365-4C8B-96EE-26026514FEE2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Abstractions", "MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj", "{FE20ED20-0D55-4D74-822B-E2AC7A54C487}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.JsonSerializerContextGenerator", "LibMatrix\Utilities\LibMatrix.JsonSerializerContextGenerator\LibMatrix.JsonSerializerContextGenerator.csproj", "{CC836863-0EE8-44BD-BF39-45076F57C416}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "LibMatrix\Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{5CE239F8-C124-4A96-A0F8-B56B9AE27434}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "LibMatrix\Tests\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{6D93DA72-69D8-43BD-BC19-7FFF8A313971}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "LibMatrix\ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.DmSpaced", "MatrixUtils.DmSpaced\MatrixUtils.DmSpaced.csproj", "{B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "LibMatrix\ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "LibMatrix\ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "LibMatrix\ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{FDB12B6A-01AA-46C0-A55E-0F984496AB81}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "LibMatrix\ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{84EE78FF-E198-4090-BFE9-C47D266E115E}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLib.Tests", "LibMatrix\ArcaneLibs\ArcaneLib.Tests\ArcaneLib.Tests.csproj", "{1607FCA9-7B5B-45B0-8D1F-205ABACB7173}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.MxApiExtensions", "LibMatrix\LibMatrix.MxApiExtensions\LibMatrix.MxApiExtensions.csproj", "{56A42391-4514-4352-B22B-622EE7A618AA}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|Any CPU.Build.0 = Release|Any CPU
- {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|Any CPU.Build.0 = Release|Any CPU
- {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.Build.0 = Release|Any CPU
- {F4E241C3-0300-4B87-8707-BCBDEF1F0185}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F4E241C3-0300-4B87-8707-BCBDEF1F0185}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F4E241C3-0300-4B87-8707-BCBDEF1F0185}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F4E241C3-0300-4B87-8707-BCBDEF1F0185}.Release|Any CPU.Build.0 = Release|Any CPU
- {41200A7B-D2DB-4656-B66B-5206A63B367A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {41200A7B-D2DB-4656-B66B-5206A63B367A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {41200A7B-D2DB-4656-B66B-5206A63B367A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {41200A7B-D2DB-4656-B66B-5206A63B367A}.Release|Any CPU.Build.0 = Release|Any CPU
- {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}.Release|Any CPU.Build.0 = Release|Any CPU
- {2D6F31D7-3139-44EC-9D11-486282DD4ED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2D6F31D7-3139-44EC-9D11-486282DD4ED1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2D6F31D7-3139-44EC-9D11-486282DD4ED1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2D6F31D7-3139-44EC-9D11-486282DD4ED1}.Release|Any CPU.Build.0 = Release|Any CPU
- {99C016AA-AFBA-4D32-A687-D1FABC0F5212}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {99C016AA-AFBA-4D32-A687-D1FABC0F5212}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {99C016AA-AFBA-4D32-A687-D1FABC0F5212}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {99C016AA-AFBA-4D32-A687-D1FABC0F5212}.Release|Any CPU.Build.0 = Release|Any CPU
- {C298E274-5D6C-47C8-9B71-A6B34D0195A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C298E274-5D6C-47C8-9B71-A6B34D0195A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C298E274-5D6C-47C8-9B71-A6B34D0195A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C298E274-5D6C-47C8-9B71-A6B34D0195A3}.Release|Any CPU.Build.0 = Release|Any CPU
- {2CB12623-4918-4176-9B4A-88D846CCD3ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2CB12623-4918-4176-9B4A-88D846CCD3ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2CB12623-4918-4176-9B4A-88D846CCD3ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2CB12623-4918-4176-9B4A-88D846CCD3ED}.Release|Any CPU.Build.0 = Release|Any CPU
- {48DBB05F-B007-4B24-89B3-3CC177C79007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {48DBB05F-B007-4B24-89B3-3CC177C79007}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {48DBB05F-B007-4B24-89B3-3CC177C79007}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {48DBB05F-B007-4B24-89B3-3CC177C79007}.Release|Any CPU.Build.0 = Release|Any CPU
- {5DEF66C8-B931-435F-B4B5-E1858590D52E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5DEF66C8-B931-435F-B4B5-E1858590D52E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5DEF66C8-B931-435F-B4B5-E1858590D52E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5DEF66C8-B931-435F-B4B5-E1858590D52E}.Release|Any CPU.Build.0 = Release|Any CPU
- {D8E5C678-3BE5-470C-A3A5-B5D525FC2012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D8E5C678-3BE5-470C-A3A5-B5D525FC2012}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D8E5C678-3BE5-470C-A3A5-B5D525FC2012}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D8E5C678-3BE5-470C-A3A5-B5D525FC2012}.Release|Any CPU.Build.0 = Release|Any CPU
- {95052EE6-7513-46FB-91BD-EE82026B42F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {95052EE6-7513-46FB-91BD-EE82026B42F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {95052EE6-7513-46FB-91BD-EE82026B42F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {95052EE6-7513-46FB-91BD-EE82026B42F1}.Release|Any CPU.Build.0 = Release|Any CPU
- {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}.Release|Any CPU.Build.0 = Release|Any CPU
- {F3312DE9-4335-4E85-A4CF-2616427A651E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F3312DE9-4335-4E85-A4CF-2616427A651E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F3312DE9-4335-4E85-A4CF-2616427A651E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F3312DE9-4335-4E85-A4CF-2616427A651E}.Release|Any CPU.Build.0 = Release|Any CPU
- {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.Build.0 = Release|Any CPU
- {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Release|Any CPU.Build.0 = Release|Any CPU
- {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.Build.0 = Release|Any CPU
- {CC836863-0EE8-44BD-BF39-45076F57C416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CC836863-0EE8-44BD-BF39-45076F57C416}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CC836863-0EE8-44BD-BF39-45076F57C416}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CC836863-0EE8-44BD-BF39-45076F57C416}.Release|Any CPU.Build.0 = Release|Any CPU
- {5CE239F8-C124-4A96-A0F8-B56B9AE27434}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5CE239F8-C124-4A96-A0F8-B56B9AE27434}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5CE239F8-C124-4A96-A0F8-B56B9AE27434}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5CE239F8-C124-4A96-A0F8-B56B9AE27434}.Release|Any CPU.Build.0 = Release|Any CPU
- {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Release|Any CPU.Build.0 = Release|Any CPU
- {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Release|Any CPU.Build.0 = Release|Any CPU
- {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Release|Any CPU.Build.0 = Release|Any CPU
- {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}.Release|Any CPU.Build.0 = Release|Any CPU
- {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}.Release|Any CPU.Build.0 = Release|Any CPU
- {FDB12B6A-01AA-46C0-A55E-0F984496AB81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FDB12B6A-01AA-46C0-A55E-0F984496AB81}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FDB12B6A-01AA-46C0-A55E-0F984496AB81}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FDB12B6A-01AA-46C0-A55E-0F984496AB81}.Release|Any CPU.Build.0 = Release|Any CPU
- {84EE78FF-E198-4090-BFE9-C47D266E115E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {84EE78FF-E198-4090-BFE9-C47D266E115E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {84EE78FF-E198-4090-BFE9-C47D266E115E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {84EE78FF-E198-4090-BFE9-C47D266E115E}.Release|Any CPU.Build.0 = Release|Any CPU
- {1607FCA9-7B5B-45B0-8D1F-205ABACB7173}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1607FCA9-7B5B-45B0-8D1F-205ABACB7173}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1607FCA9-7B5B-45B0-8D1F-205ABACB7173}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1607FCA9-7B5B-45B0-8D1F-205ABACB7173}.Release|Any CPU.Build.0 = Release|Any CPU
- {56A42391-4514-4352-B22B-622EE7A618AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {56A42391-4514-4352-B22B-622EE7A618AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {56A42391-4514-4352-B22B-622EE7A618AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {56A42391-4514-4352-B22B-622EE7A618AA}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {F4E241C3-0300-4B87-8707-BCBDEF1F0185} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}
- {41200A7B-D2DB-4656-B66B-5206A63B367A} = {F1376F9A-FB65-4E60-BB9A-62A64F741FF4}
- {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14}
- {2D6F31D7-3139-44EC-9D11-486282DD4ED1} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14}
- {99C016AA-AFBA-4D32-A687-D1FABC0F5212} = {F1376F9A-FB65-4E60-BB9A-62A64F741FF4}
- {C298E274-5D6C-47C8-9B71-A6B34D0195A3} = {F1376F9A-FB65-4E60-BB9A-62A64F741FF4}
- {3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}
- {2CB12623-4918-4176-9B4A-88D846CCD3ED} = {3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2}
- {48DBB05F-B007-4B24-89B3-3CC177C79007} = {3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2}
- {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}
- {5DEF66C8-B931-435F-B4B5-E1858590D52E} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D}
- {D8E5C678-3BE5-470C-A3A5-B5D525FC2012} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D}
- {95052EE6-7513-46FB-91BD-EE82026B42F1} = {3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2}
- {7D2C9959-8309-4110-A67F-DEE64E97C1D8} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}
- {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881} = {7D2C9959-8309-4110-A67F-DEE64E97C1D8}
- {F3312DE9-4335-4E85-A4CF-2616427A651E} = {7D2C9959-8309-4110-A67F-DEE64E97C1D8}
- {1CAA2B6D-0365-4C8B-96EE-26026514FEE2} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}
- {CC836863-0EE8-44BD-BF39-45076F57C416} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D}
- {5CE239F8-C124-4A96-A0F8-B56B9AE27434} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D}
- {6D93DA72-69D8-43BD-BC19-7FFF8A313971} = {7D2C9959-8309-4110-A67F-DEE64E97C1D8}
- {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14}
- {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14}
- {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14}
- {FDB12B6A-01AA-46C0-A55E-0F984496AB81} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14}
- {84EE78FF-E198-4090-BFE9-C47D266E115E} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14}
- {1607FCA9-7B5B-45B0-8D1F-205ABACB7173} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14}
- {56A42391-4514-4352-B22B-622EE7A618AA} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}
- EndGlobalSection
-EndGlobal
diff --git a/MatrixUtils.Abstractions/FileStorageProvider.cs b/MatrixUtils.Abstractions/FileStorageProvider.cs
index fbe068d..1083002 100644
--- a/MatrixUtils.Abstractions/FileStorageProvider.cs
+++ b/MatrixUtils.Abstractions/FileStorageProvider.cs
@@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using ArcaneLibs.Extensions;
-using LibMatrix.Extensions;
using LibMatrix.Interfaces.Services;
using Microsoft.Extensions.Logging;
diff --git a/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj b/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj
index 1665ff0..96f9fcb 100644
--- a/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj
+++ b/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj
@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
-
+
<ItemGroup>
- <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
+ <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
</ItemGroup>
</Project>
diff --git a/MatrixUtils.Abstractions/RoomInfo.cs b/MatrixUtils.Abstractions/RoomInfo.cs
index aff0e25..4b2a53c 100644
--- a/MatrixUtils.Abstractions/RoomInfo.cs
+++ b/MatrixUtils.Abstractions/RoomInfo.cs
@@ -3,7 +3,6 @@ using System.Collections.ObjectModel;
using System.Text.Json.Nodes;
using ArcaneLibs;
using LibMatrix;
-using LibMatrix.EventTypes.Spec.State;
using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Homeservers;
using LibMatrix.RoomTypes;
@@ -13,13 +12,13 @@ namespace MatrixUtils.Abstractions;
public class RoomInfo : NotifyPropertyChanged {
public RoomInfo(GenericRoom room) {
Room = room;
- _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId);
+ // _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId);
RegisterEventListener();
}
public RoomInfo(GenericRoom room, List<StateEventResponse>? stateEvents) {
Room = room;
- _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId);
+ // _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId);
if (stateEvents is { Count: > 0 }) StateEvents = new(stateEvents!);
RegisterEventListener();
ProcessNewItems(stateEvents!);
@@ -30,7 +29,7 @@ public class RoomInfo : NotifyPropertyChanged {
public ObservableCollection<StateEventResponse?> Timeline { get; private set; } = new();
private static ConcurrentBag<AuthenticatedHomeserverGeneric> homeserversWithoutEventFormatSupport = new();
- private static SvgIdenticonGenerator identiconGenerator = new();
+ // private static SvgIdenticonGenerator identiconGenerator = new();
public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") {
if (homeserversWithoutEventFormatSupport.Contains(Room.Homeserver)) return await GetStateEventForged(type, stateKey);
@@ -96,7 +95,7 @@ public class RoomInfo : NotifyPropertyChanged {
}
public string? RoomIcon {
- get => _roomIcon ?? _fallbackIcon;
+ get => _roomIcon;
set => SetField(ref _roomIcon, value);
}
diff --git a/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs b/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs
index 1e4a127..1e6b99f 100644
--- a/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs
+++ b/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs
@@ -44,7 +44,7 @@ public partial class RoomListEntry : UserControl {
var avatarEvent = await Room.GetStateEvent("m.room.avatar");
if (avatarEvent?.TypedContent is RoomAvatarEventContent avatarData) {
var mxcUrl = avatarData.Url;
- var resolvedUrl = await Room.Room.GetResolvedRoomAvatarUrlAsync();
+ var resolvedUrl = await Room.Room.GetAvatarUrlAsync();
// await using var svc = _serviceScopeFactory.CreateAsyncScope();
// var hs = await svc.ServiceProvider.GetService<RMUStorageWrapper>()?.GetCurrentSessionOrPrompt()!;
@@ -54,10 +54,10 @@ public partial class RoomListEntry : UserControl {
var storage = new FileStorageProvider("cache");
var storageKey = $"media/{mxcUrl.Replace("mxc://", "").Replace("/", ".")}";
try {
- if (!await storage.ObjectExistsAsync(storageKey))
- await storage.SaveStreamAsync(storageKey, await hc.GetStreamAsync(resolvedUrl));
+ // if (!await storage.ObjectExistsAsync(storageKey))
+ // await storage.SaveStreamAsync(storageKey, await hc.GetStreamAsync(resolvedUrl));
- RoomIcon.Source = new Bitmap(await storage.LoadStreamAsync(storageKey) ?? throw new NullReferenceException());
+ // RoomIcon.Source = new Bitmap(await storage.LoadStreamAsync(storageKey) ?? throw new NullReferenceException());
}
catch (IOException) { }
catch (MatrixException e) {
diff --git a/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj b/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj
index ce009d5..a7ff4b9 100644
--- a/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj
+++ b/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
@@ -20,21 +20,21 @@
<ItemGroup>
- <PackageReference Include="Avalonia" Version="11.0.10" />
- <PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
- <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
- <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
+ <PackageReference Include="Avalonia" Version="11.2.1" />
+ <PackageReference Include="Avalonia.Desktop" Version="11.2.1" />
+ <PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1" />
+ <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.1" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
- <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
- <PackageReference Include="Sentry" Version="4.7.0" />
+ <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.1" />
+ <PackageReference Include="Sentry" Version="4.13.0" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.10.9" />
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
+ <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.2.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings*.json">
diff --git a/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj
index 4b0f599..f8dd598 100644
--- a/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj
+++ b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
diff --git a/MatrixUtils.DmSpaced/Program.cs b/MatrixUtils.DmSpaced/Program.cs
index ae352b7..6ed6cbc 100644
--- a/MatrixUtils.DmSpaced/Program.cs
+++ b/MatrixUtils.DmSpaced/Program.cs
@@ -22,17 +22,17 @@ if (Environment.GetEnvironmentVariable("MODERATIONBOT_APPSETTINGS_PATH") is stri
builder.ConfigureAppConfiguration(x => x.AddJsonFile(path));
var host = builder.ConfigureServices((_, services) => {
- services.AddScoped<TieredStorageService>(x =>
- new TieredStorageService(
- cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),
- dataStorageProvider: new FileStorageProvider("bot_data/data/")
- )
- );
+ // services.AddScoped<TieredStorageService>(x =>
+ // new TieredStorageService(
+ // cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),
+ // dataStorageProvider: new FileStorageProvider("bot_data/data/")
+ // )
+ // );
services.AddSingleton<ModerationBotConfiguration>();
services.AddRoryLibMatrixServices();
- services.AddSingleton<ModerationBotRoomProvider>();
+ // services.AddSingleton<ModerationBotRoomProvider>();
services.AddHostedService<ModerationBot.ModerationBot>();
}).UseConsoleLifetime().Build();
diff --git a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
index e2c8192..646a3f3 100644
--- a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
+++ b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
@@ -1,7 +1,5 @@
-using System.Net;
using ArcaneLibs.Extensions;
using LibMatrix;
-using LibMatrix.EventTypes.Spec.State;
using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Homeservers;
using LibMatrix.Responses;
diff --git a/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj b/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj
index 72c1666..e39440e 100644
--- a/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj
+++ b/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LinkIncremental>true</LinkIncremental>
@@ -10,6 +10,6 @@
</PropertyGroup>
<ItemGroup>
- <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj"/>
+ <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
</ItemGroup>
</Project>
diff --git a/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs b/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs
index bc595b5..f7e1e20 100644
--- a/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs
+++ b/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs
@@ -1,6 +1,5 @@
using System.Text.Json.Serialization;
using LibMatrix.EventTypes;
-using LibMatrix.Interfaces;
namespace MatrixUtils.LibDMSpace.StateEvents;
diff --git a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs
index 16c7b70..886c34d 100644
--- a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs
+++ b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs
@@ -1,6 +1,5 @@
using System.Text.Json.Serialization;
using LibMatrix.EventTypes;
-using LibMatrix.Interfaces;
namespace MatrixUtils.LibDMSpace.StateEvents;
diff --git a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs
index f5daa74..170efc7 100644
--- a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs
+++ b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs
@@ -1,6 +1,5 @@
using System.Text.Json.Serialization;
using LibMatrix.EventTypes;
-using LibMatrix.Interfaces;
namespace MatrixUtils.LibDMSpace.StateEvents;
diff --git a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
index f2d47ea..24401ab 100644
--- a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
+++ b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.6" />
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.1" />
</ItemGroup>
<ItemGroup>
diff --git a/MatrixUtils.Web/App.razor b/MatrixUtils.Web/App.razor
index 5e87bc3..a8cf817 100644
--- a/MatrixUtils.Web/App.razor
+++ b/MatrixUtils.Web/App.razor
@@ -1,5 +1,4 @@
-@using Microsoft.AspNetCore.Components.Routing
-<Router AppAssembly="@typeof(App).Assembly">
+<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
diff --git a/MatrixUtils.Web/Classes/Constants/RoomConstants.cs b/MatrixUtils.Web/Classes/Constants/RoomConstants.cs
index 5df0d01..dc81d04 100644
--- a/MatrixUtils.Web/Classes/Constants/RoomConstants.cs
+++ b/MatrixUtils.Web/Classes/Constants/RoomConstants.cs
@@ -1,6 +1,7 @@
namespace MatrixUtils.Web.Classes.Constants;
public class RoomConstants {
- public static readonly string[] DangerousRoomVersions = { "1", "8" };
- public const string RecommendedRoomVersion = "10";
+ public static readonly string[] DangerousRoomVersions = ["1", "8"];
+ public static readonly string[] UnsupportedRoomVersions = ["1", "2", "3", "4", "5", "6"];
+ public const string RecommendedRoomVersion = "11";
}
diff --git a/MatrixUtils.Web/Classes/LocalStorageProviderService.cs b/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
index 3803a17..0e99cd4 100644
--- a/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
+++ b/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
@@ -22,7 +22,7 @@ public class LocalStorageProviderService : IStorageProvider {
async Task<bool> IStorageProvider.ObjectExistsAsync(string key) => await _localStorageService.ContainKeyAsync(key);
- async Task<List<string>> IStorageProvider.GetAllKeysAsync() => (await _localStorageService.KeysAsync()).ToList();
+ async Task<IEnumerable<string>> IStorageProvider.GetAllKeysAsync() => (await _localStorageService.KeysAsync()).ToList();
async Task IStorageProvider.DeleteObjectAsync(string key) => await _localStorageService.RemoveItemAsync(key);
}
diff --git a/MatrixUtils.Web/Classes/RMUStorageWrapper.cs b/MatrixUtils.Web/Classes/RMUStorageWrapper.cs
deleted file mode 100644
index 45028ba..0000000
--- a/MatrixUtils.Web/Classes/RMUStorageWrapper.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using LibMatrix;
-using LibMatrix.Homeservers;
-using LibMatrix.Services;
-using Microsoft.AspNetCore.Components;
-
-namespace MatrixUtils.Web.Classes;
-
-public class RMUStorageWrapper(ILogger<RMUStorageWrapper> logger, TieredStorageService storageService, HomeserverProviderService homeserverProviderService, NavigationManager navigationManager) {
- public async Task<List<UserAuth>?> GetAllTokens() {
- logger.LogTrace("Getting all tokens.");
- return await storageService.DataStorageProvider.LoadObjectAsync<List<UserAuth>>("rmu.tokens") ??
- new List<UserAuth>();
- }
-
- public async Task<UserAuth?> GetCurrentToken() {
- logger.LogTrace("Getting current token.");
- var currentToken = await storageService.DataStorageProvider.LoadObjectAsync<UserAuth>("rmu.token");
- var allTokens = await GetAllTokens();
- if (allTokens is null or { Count: 0 }) {
- await SetCurrentToken(null);
- return null;
- }
-
- if (currentToken is null) {
- await SetCurrentToken(currentToken = allTokens[0]);
- }
-
- if (!allTokens.Any(x => x.AccessToken == currentToken.AccessToken)) {
- await SetCurrentToken(currentToken = allTokens[0]);
- }
-
- return currentToken;
- }
-
- public async Task AddToken(UserAuth UserAuth) {
- logger.LogTrace("Adding token.");
- var tokens = await GetAllTokens() ?? new List<UserAuth>();
-
- tokens.Add(UserAuth);
- await storageService.DataStorageProvider.SaveObjectAsync("rmu.tokens", tokens);
- }
-
- private async Task<AuthenticatedHomeserverGeneric?> GetCurrentSession() {
- logger.LogTrace("Getting current session.");
- var token = await GetCurrentToken();
- if (token == null) {
- return null;
- }
-
- return await homeserverProviderService.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy);
- }
-
- public async Task<AuthenticatedHomeserverGeneric?> GetSession(UserAuth userAuth) {
- logger.LogTrace("Getting session.");
- return await homeserverProviderService.GetAuthenticatedWithToken(userAuth.Homeserver, userAuth.AccessToken, userAuth.Proxy);
- }
-
- public async Task<AuthenticatedHomeserverGeneric?> GetCurrentSessionOrNavigate() {
- logger.LogTrace("Getting current session or navigating.");
- AuthenticatedHomeserverGeneric? session = null;
-
- try {
- //catch if the token is invalid
- session = await GetCurrentSession();
- }
- catch (MatrixException e) {
- if (e.ErrorCode == "M_UNKNOWN_TOKEN") {
- var token = await GetCurrentToken();
- logger.LogWarning("Encountered invalid token for {user} on {homeserver}", token.UserId, token.Homeserver);
- navigationManager.NavigateTo("/InvalidSession?ctx=" + token.AccessToken);
- return null;
- }
-
- throw;
- }
-
- if (session is null) {
- logger.LogInformation("No session found. Navigating to login.");
- navigationManager.NavigateTo("/Login");
- }
-
- return session;
- }
-
- public class Settings {
- public DeveloperSettings DeveloperSettings { get; set; } = new();
- }
-
- public class DeveloperSettings {
- public bool EnableLogViewers { get; set; }
- public bool EnableConsoleLogging { get; set; } = true;
- public bool EnablePortableDevtools { get; set; }
- }
-
- public async Task RemoveToken(UserAuth auth) {
- logger.LogTrace("Removing token.");
- var tokens = await GetAllTokens();
- if (tokens == null) {
- return;
- }
-
- tokens.RemoveAll(x => x.AccessToken == auth.AccessToken);
- await storageService.DataStorageProvider.SaveObjectAsync("rmu.tokens", tokens);
- }
-
- public async Task SetCurrentToken(UserAuth? auth) {
- logger.LogTrace("Setting current token.");
- await storageService.DataStorageProvider.SaveObjectAsync("rmu.token", auth);
- }
-
- public async Task MigrateFromMRU() {
- logger.LogInformation("Migrating from MRU token namespace!");
- var dsp = storageService.DataStorageProvider!;
- if(await dsp.ObjectExistsAsync("token")) {
- var oldToken = await dsp.LoadObjectAsync<UserAuth>("token");
- if (oldToken != null) {
- await dsp.SaveObjectAsync("rmu.token", oldToken);
- await dsp.DeleteObjectAsync("tokens");
- }
- }
-
- if(await dsp.ObjectExistsAsync("tokens")) {
- var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("tokens");
- if (oldTokens != null) {
- await dsp.SaveObjectAsync("rmu.tokens", oldTokens);
- await dsp.DeleteObjectAsync("tokens");
- }
- }
-
- if(await dsp.ObjectExistsAsync("mru.tokens")) {
- var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("mru.tokens");
- if (oldTokens != null) {
- await dsp.SaveObjectAsync("rmu.tokens", oldTokens);
- await dsp.DeleteObjectAsync("mru.tokens");
- }
- }
- }
-}
diff --git a/MatrixUtils.Web/Classes/RmuSessionStore.cs b/MatrixUtils.Web/Classes/RmuSessionStore.cs
new file mode 100644
index 0000000..746f68a
--- /dev/null
+++ b/MatrixUtils.Web/Classes/RmuSessionStore.cs
@@ -0,0 +1,260 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using LibMatrix;
+using LibMatrix.Homeservers;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Components;
+
+namespace MatrixUtils.Web.Classes;
+
+public class RmuSessionStore(
+ ILogger<RmuSessionStore> logger,
+ TieredStorageService storageService,
+ HomeserverProviderService homeserverProviderService,
+ NavigationManager navigationManager) {
+ private SessionInfo? CurrentSession { get; set; }
+ private Dictionary<string, SessionInfo> SessionCache { get; set; } = [];
+
+ private bool _isInitialized;
+ private static readonly SemaphoreSlim InitSemaphore = new(1, 1);
+
+#region Sessions
+
+ public async Task<Dictionary<string, SessionInfo>> GetAllSessions() {
+ await LoadStorage();
+ logger.LogTrace("Getting all tokens.");
+ return SessionCache;
+ }
+
+ public async Task<SessionInfo?> GetSession(string sessionId) {
+ await LoadStorage();
+ if (SessionCache.TryGetValue(sessionId, out var cachedSession))
+ return cachedSession;
+
+ logger.LogWarning("Session {sessionId} not found in all tokens.", sessionId);
+ return null;
+ }
+
+ public async Task<SessionInfo?> GetCurrentSession(bool log = true) {
+ await LoadStorage();
+ if (log) logger.LogTrace("Getting current token.");
+ if (CurrentSession is not null) return CurrentSession;
+
+ var currentSessionId = await storageService.DataStorageProvider!.LoadObjectAsync<string>("rmu.session");
+ return await GetSession(currentSessionId);
+ }
+
+ public async Task<string> AddSession(UserAuth auth) {
+ await LoadStorage();
+ logger.LogTrace("Adding token.");
+
+ var sessionId = auth.GetHashCode().ToString();
+ SessionCache[sessionId] = new() {
+ Auth = auth,
+ SessionId = sessionId
+ };
+
+ if (CurrentSession == null) await SetCurrentSession(sessionId);
+ else await SaveStorage();
+
+ return sessionId;
+ }
+
+ public async Task RemoveSession(string sessionId) {
+ await LoadStorage();
+ logger.LogTrace("Removing session {sessionId}.", sessionId);
+ var tokens = await GetAllSessions();
+ if (tokens == null) {
+ return;
+ }
+
+ if ((await GetCurrentSession())?.SessionId == sessionId)
+ await SetCurrentSession(tokens.First(x => x.Key != sessionId).Key);
+
+ if (tokens.Remove(sessionId))
+ await SaveStorage();
+ }
+
+ public async Task SetCurrentSession(string? sessionId) {
+ await LoadStorage();
+ logger.LogTrace("Setting current session to {sessionId}.", sessionId);
+ CurrentSession = await GetSession(sessionId);
+ await SaveStorage();
+ }
+
+#endregion
+
+#region Homeservers
+
+ public async Task<AuthenticatedHomeserverGeneric?> GetHomeserver(string session, bool log = true) {
+ await LoadStorage();
+ if (log) logger.LogTrace("Getting session.");
+ if (!SessionCache.TryGetValue(session, out var cachedSession)) return null;
+ if (cachedSession.Homeserver is not null) return cachedSession.Homeserver;
+
+ try {
+ cachedSession.Homeserver =
+ await homeserverProviderService.GetAuthenticatedWithToken(cachedSession.Auth.Homeserver, cachedSession.Auth.AccessToken, cachedSession.Auth.Proxy);
+ }
+ catch (Exception e) {
+ logger.LogError("Failed to get info for {0} via {1}: {2}", cachedSession.Auth.UserId, cachedSession.Auth.Homeserver, e);
+ logger.LogError("Continuing with server-less session");
+ cachedSession.Homeserver = await homeserverProviderService.GetAuthenticatedWithToken(cachedSession.Auth.Homeserver, cachedSession.Auth.AccessToken,
+ cachedSession.Auth.Proxy, useGeneric: true, enableServer: false);
+ }
+
+ return cachedSession.Homeserver;
+ }
+
+ public async Task<AuthenticatedHomeserverGeneric?> GetCurrentHomeserver(bool log = true, bool navigateOnFailure = false) {
+ await LoadStorage();
+ if (log) logger.LogTrace("Getting current session.");
+ if (CurrentSession?.Homeserver is not null) return CurrentSession.Homeserver;
+
+ var currentSession = CurrentSession ??= await GetCurrentSession(log: false);
+ if (currentSession == null) {
+ if (navigateOnFailure) {
+ logger.LogInformation("No session found. Navigating to login.");
+ navigationManager.NavigateTo("/Login");
+ }
+
+ return null;
+ }
+
+ try {
+ return currentSession.Homeserver ??= await GetHomeserver(currentSession.SessionId);
+ }
+ catch (MatrixException e) {
+ if (e.ErrorCode == "M_UNKNOWN_TOKEN" && navigateOnFailure) {
+ logger.LogWarning("Encountered invalid token for {user} on {homeserver}", currentSession.Auth.UserId, currentSession.Auth.Homeserver);
+ if (navigateOnFailure) {
+ navigationManager.NavigateTo("/InvalidSession?ctx=" + currentSession.SessionId);
+ }
+ }
+
+ throw;
+ }
+ }
+
+#endregion
+
+#region Storage
+
+ private async Task LoadStorage(bool hasMigrated = false) {
+ if (!await storageService.DataStorageProvider!.ObjectExistsAsync("rmu.sessions") || !await storageService.DataStorageProvider.ObjectExistsAsync("rmu.session")) {
+ if (!hasMigrated) {
+ await RunMigrations();
+ await LoadStorage(true);
+ }
+ else
+ logger.LogWarning("No sessions found in storage.");
+
+ return;
+ }
+
+ SessionCache = (await storageService.DataStorageProvider.LoadObjectAsync<Dictionary<string, UserAuth>>("rmu.sessions") ?? throw new Exception("Failed to load sessions"))
+ .ToDictionary(x => x.Key, x => new SessionInfo {
+ SessionId = x.Key,
+ Auth = x.Value
+ });
+
+ var currentSessionId = await storageService.DataStorageProvider.LoadObjectAsync<string>("rmu.session");
+ if (currentSessionId == null) {
+ logger.LogWarning("No current session found in storage.");
+ return;
+ }
+
+ if (!SessionCache.TryGetValue(currentSessionId, out var currentSession)) {
+ logger.LogWarning("Current session {currentSessionId} not found in storage.", currentSessionId);
+ return;
+ }
+
+ CurrentSession = currentSession;
+ }
+
+ private async Task SaveStorage() {
+ await storageService.DataStorageProvider!.SaveObjectAsync("rmu.sessions",
+ SessionCache.ToDictionary(
+ x => x.Key,
+ x => x.Value.Auth
+ )
+ );
+ await storageService.DataStorageProvider.SaveObjectAsync("rmu.session", CurrentSession?.SessionId);
+ }
+
+#endregion
+
+#region Migrations
+
+ public async Task RunMigrations() {
+ await MigrateFromMru();
+ await MigrateAccountsToKeyedStorage();
+ }
+
+ private async Task MigrateFromMru() {
+ logger.LogInformation("Migrating from MRU token namespace!");
+ var dsp = storageService.DataStorageProvider!;
+ if (await dsp.ObjectExistsAsync("token")) {
+ var oldToken = await dsp.LoadObjectAsync<UserAuth>("token");
+ if (oldToken != null) {
+ await dsp.SaveObjectAsync("rmu.token", oldToken);
+ await dsp.DeleteObjectAsync("token");
+ }
+ }
+
+ if (await dsp.ObjectExistsAsync("tokens")) {
+ var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("tokens");
+ if (oldTokens != null) {
+ await dsp.SaveObjectAsync("rmu.tokens", oldTokens);
+ await dsp.DeleteObjectAsync("tokens");
+ }
+ }
+
+ if (await dsp.ObjectExistsAsync("mru.tokens")) {
+ var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("mru.tokens");
+ if (oldTokens != null) {
+ await dsp.SaveObjectAsync("rmu.tokens", oldTokens);
+ await dsp.DeleteObjectAsync("mru.tokens");
+ }
+ }
+ }
+
+ private async Task MigrateAccountsToKeyedStorage() {
+ var dsp = storageService.DataStorageProvider!;
+ if (!await dsp.ObjectExistsAsync("rmu.tokens")) return;
+ logger.LogInformation("Migrating accounts to keyed storage!");
+ var tokens = await dsp.LoadObjectAsync<UserAuth[]>("rmu.tokens") ?? throw new Exception("Failed to load tokens");
+ Dictionary<string, UserAuth> keyedTokens = tokens.ToDictionary(x => x.GetHashCode().ToString(), x => x);
+
+ if (await dsp.ObjectExistsAsync("rmu.token")) {
+ var token = await dsp.LoadObjectAsync<UserAuth>("rmu.token") ?? throw new Exception("Failed to load token");
+ var sessionId = keyedTokens.FirstOrDefault(x => x.Value.Equals(token)).Key;
+
+ if (sessionId is null) keyedTokens.Add(sessionId = token.GetHashCode().ToString(), token);
+ await dsp.SaveObjectAsync("rmu.session", sessionId);
+
+ await dsp.DeleteObjectAsync("rmu.token");
+ }
+
+ await dsp.SaveObjectAsync("rmu.sessions", keyedTokens);
+ await dsp.DeleteObjectAsync("rmu.tokens");
+ }
+
+#endregion
+
+ public class Settings {
+ public DeveloperSettings DeveloperSettings { get; set; } = new();
+ }
+
+ public class DeveloperSettings {
+ public bool EnableLogViewers { get; set; }
+ public bool EnableConsoleLogging { get; set; } = true;
+ public bool EnablePortableDevtools { get; set; }
+ }
+
+ public class SessionInfo {
+ public required string SessionId { get; set; }
+ public required UserAuth Auth { get; set; }
+ public AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs b/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
index a627a9c..7078308 100644
--- a/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
+++ b/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
@@ -1,6 +1,5 @@
using System.Text.Json.Nodes;
using LibMatrix;
-using LibMatrix.EventTypes.Spec.State;
using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Responses;
@@ -34,7 +33,7 @@ public class DefaultRoomCreationTemplate : IRoomCreationTemplate {
},
new() {
Type = "m.room.server_acl",
- TypedContent = new RoomServerACLEventContent() {
+ TypedContent = new RoomServerAclEventContent() {
Allow = new List<string>() { "*" },
Deny = new List<string>(),
AllowIpLiterals = false
@@ -56,7 +55,7 @@ public class DefaultRoomCreationTemplate : IRoomCreationTemplate {
Redact = 50,
Kick = 50,
Ban = 50,
- NotificationsPl = new RoomPowerLevelEventContent.NotificationsPL {
+ NotificationsPl = new RoomPowerLevelEventContent.NotificationsPowerLevels {
Room = 50
},
Events = new() {
diff --git a/MatrixUtils.Web/Classes/SessionStorageProviderService.cs b/MatrixUtils.Web/Classes/SessionStorageProviderService.cs
index ae0bb79..da169de 100644
--- a/MatrixUtils.Web/Classes/SessionStorageProviderService.cs
+++ b/MatrixUtils.Web/Classes/SessionStorageProviderService.cs
@@ -22,7 +22,7 @@ public class SessionStorageProviderService : IStorageProvider {
async Task<bool> IStorageProvider.ObjectExistsAsync(string key) => await _sessionStorageService.ContainKeyAsync(key);
- async Task<List<string>> IStorageProvider.GetAllKeysAsync() => (await _sessionStorageService.KeysAsync()).ToList();
+ async Task<IEnumerable<string>> IStorageProvider.GetAllKeysAsync() => (await _sessionStorageService.KeysAsync()).ToList();
async Task IStorageProvider.DeleteObjectAsync(string key) => await _sessionStorageService.RemoveItemAsync(key);
}
diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj
index 8760e7a..aa9f37f 100644
--- a/MatrixUtils.Web/MatrixUtils.Web.csproj
+++ b/MatrixUtils.Web/MatrixUtils.Web.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LinkIncremental>true</LinkIncremental>
@@ -12,62 +12,65 @@
<BlazorEnableCompression>false</BlazorEnableCompression>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
<BlazorCacheBootResources>false</BlazorCacheBootResources>
-<!-- <RunAOTCompilation>true</RunAOTCompilation>-->
+ <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
</PropertyGroup>
- <ItemGroup>
- <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
- <PackageReference Include="Blazored.SessionStorage" Version="2.4.0" />
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all" />
- <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.0" />
- </ItemGroup>
+ <!-- Explicitly disable all the unused runtime things trimming would have removed anyways -->
+ <!-- https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options -->
+<!-- <PropertyGroup>-->
+<!-- <AutoreleasePoolSupport>false</AutoreleasePoolSupport> <!– Browser != MacOS –>-->
+<!-- <MetadataUpdaterSupport>false</MetadataUpdaterSupport> <!– Unreliable –>-->
+<!-- <DebuggerSupport>false</DebuggerSupport> <!– Unreliable –>-->
+<!-- <InvariantGlobalization>true</InvariantGlobalization> <!– invariant globalization is fine –>-->
+<!-- <!– unused features –>-->
+<!-- <EventSourceSupport>false</EventSourceSupport>-->
+<!-- <EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>-->
+<!-- <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>-->
+<!-- <EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>-->
+<!-- <MetricsSupport>false</MetricsSupport>-->
+<!-- <UseNativeHttpHandler>false</UseNativeHttpHandler>-->
+<!-- <XmlResolverIsNetworkingEnabledByDefault>false</XmlResolverIsNetworkingEnabledByDefault>-->
+<!-- <BuiltInComInteropSupport>false</BuiltInComInteropSupport>-->
+<!-- <CustomResourceTypesSupport>false</CustomResourceTypesSupport>-->
+<!-- <EnableCppCLIHostActivation>false</EnableCppCLIHostActivation>-->
+<!-- <StartupHookSupport>false</StartupHookSupport>-->
+<!-- </PropertyGroup>-->
<ItemGroup>
- <ProjectReference Condition="Exists('..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj" />
- <PackageReference Condition="!Exists('..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="ArcaneLibs.Blazor.Components" Version="*-preview*" />
- <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
- <ProjectReference Include="..\MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj" />
- <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj" />
+ <PackageReference Include="Blazored.LocalStorage" Version="4.5.0"/>
+ <PackageReference Include="Blazored.SessionStorage" Version="2.4.0"/>
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" />
+ <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.2" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.1" />
+ <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.5.39" />
</ItemGroup>
<ItemGroup>
- <Content Update="appsettings.Development.json">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
- </Content>
- <Content Update="appsettings.json">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
- </Content>
- <Content Update="wwwroot\appsettings.json">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
- </Content>
+ <ProjectReference Include="..\MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj"/>
+ <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj"/>
</ItemGroup>
<ItemGroup>
- <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
+<!-- <PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.0-preview.20241210-161342" Condition="'$(Configuration)' == 'Release'"/>-->
+<!-- <ProjectReference Include="..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj" Condition="'$(Configuration)' == 'Debug'"/>-->
+ <ProjectReference Include="..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj"/>
</ItemGroup>
<ItemGroup>
- <_ContentIncludedByDefault Remove="Pages\Client\ClientComponents\ClientRoomList.razor" />
- <_ContentIncludedByDefault Remove="Pages\Client\ClientComponents\ClientStatusList.razor" />
- <_ContentIncludedByDefault Remove="Pages\Client\ClientComponents\MatrixClient.razor" />
- <_ContentIncludedByDefault Remove="Pages\Client\Index.razor" />
+ <Content Update="appsettings.Development.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ <Content Update="appsettings.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ <Content Update="wwwroot\appsettings.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
</ItemGroup>
<ItemGroup>
- <AdditionalFiles Include="Pages\Labs\Client\ClientComponents\ClientRoomList.razor" />
- <AdditionalFiles Include="Pages\Labs\Client\ClientComponents\ClientStatusList.razor" />
- <AdditionalFiles Include="Pages\Labs\Client\ClientComponents\MatrixClient.razor" />
- <AdditionalFiles Include="Pages\Labs\Client\Index.razor" />
- <AdditionalFiles Include="Pages\Labs\DMSpace\DMSpaceStages\DMSpaceStage0.razor" />
- <AdditionalFiles Include="Pages\Labs\DMSpace\DMSpaceStages\DMSpaceStage1.razor" />
- <AdditionalFiles Include="Pages\Labs\DMSpace\DMSpaceStages\DMSpaceStage2.razor" />
- <AdditionalFiles Include="Pages\Labs\DMSpace\DMSpaceStages\DMSpaceStage3.razor" />
- <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\MainTabComponents\MainTabSpaceItem.razor" />
- <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\RoomsIndex2ByRoomTypeTab.razor" />
- <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\RoomsIndex2DMsTab.razor" />
- <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\RoomsIndex2MainTab.razor" />
- <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\RoomsIndex2SyncContainer.razor" />
+ <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js"/>
</ItemGroup>
-
+
</Project>
diff --git a/MatrixUtils.Web/Pages/About.razor b/MatrixUtils.Web/Pages/About.razor
index 18d7c3f..9f83991 100644
--- a/MatrixUtils.Web/Pages/About.razor
+++ b/MatrixUtils.Web/Pages/About.razor
@@ -7,6 +7,6 @@
<p>Rory&::MatrixUtils is a "small" collection of tools to do not-so-everyday things.</p>
<p>These range from joining rooms on dead homeservers, to managing your accounts and rooms, and creating rooms based on templates.</p>
-<br/><br/>
-<p>You can find the source code on <a href="https://cgit.rory.gay/matrix/MatrixRoomUtils.git/">cgit.rory.gay</a>.<br/></p>
+<br/>
+<p>You can find the source code on <a href="https://cgit.rory.gay/matrix/tools/MatrixUtils.git/about/">cgit.rory.gay</a>.<br/></p>
<p>You can also join the <a href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support">Matrix room</a> for this project.</p>
diff --git a/MatrixUtils.Web/Pages/Dev/DevOptions.razor b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
index 7b646d1..33e577f 100644
--- a/MatrixUtils.Web/Pages/Dev/DevOptions.razor
+++ b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
@@ -2,7 +2,6 @@
@using ArcaneLibs.Extensions
@using System.Text
@using System.Text.Json
-@using Microsoft.JSInterop
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@inject TieredStorageService TieredStorage
@@ -20,6 +19,10 @@
<span>Export local storage: </span>
<button @onclick="@ExportLocalStorage">Export</button>
</p>
+<details>
+ <summary>Manage local sessions</summary>
+
+</details>
@if (userSettings is not null) {
<InputCheckbox @bind-Value="@userSettings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox>
@@ -36,9 +39,9 @@
@code {
- private RMUStorageWrapper.Settings? userSettings { get; set; }
+ private RmuSessionStore.Settings? userSettings { get; set; }
protected override async Task OnInitializedAsync() {
- // userSettings = await TieredStorage.DataStorageProvider.LoadObjectAsync<RMUStorageWrapper.Settings>("rmu.settings");
+ // userSettings = await TieredStorage.DataStorageProvider.LoadObjectAsync<RmuSessionStore.Settings>("rmu.settings");
await base.OnInitializedAsync();
}
diff --git a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
index bf5a396..3b2d533 100644
--- a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
+++ b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
@@ -4,6 +4,7 @@
<h3>Debug Tools</h3>
<hr/>
+<LinkButton href="/Dev/WellKnownRes">Well known res tests</LinkButton>
@if (Rooms.Count == 0) {
<p>You are not in any rooms!</p>
@* <p>Loading progress: @checkedRoomCount/@totalRoomCount</p> *@
@@ -38,7 +39,7 @@ else {
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs == null) return;
Rooms = (await hs.GetJoinedRooms()).Select(x => x.RoomId).ToList();
Console.WriteLine("Fetched joined rooms!");
diff --git a/MatrixUtils.Web/Pages/Dev/ModalTest.razor b/MatrixUtils.Web/Pages/Dev/ModalTest.razor
new file mode 100644
index 0000000..665f548
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Dev/ModalTest.razor
@@ -0,0 +1,12 @@
+@page "/Dev/ModalTest"
+
+<PageTitle>Modal test</PageTitle>
+
+<h3>Rory&::MatrixUtils - Modal test</h3>
+<hr/>
+@for (int i = 0; i < 10; i++)
+{
+ <ModalWindow X="i*75" Y="i*75">
+ <h1>Hello, world!</h1>
+ </ModalWindow>
+}
diff --git a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
new file mode 100644
index 0000000..1906dd8
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
@@ -0,0 +1,123 @@
+@page "/Dev/WellKnownRes"
+@using ArcaneLibs.Extensions
+@using LibMatrix.Services.WellKnownResolver
+@using LibMatrix.Services.WellKnownResolver.WellKnownResolvers
+@inject HomeserverResolverService legacyResolver
+@inject WellKnownResolverService rewriteResolver
+@inject ClientWellKnownResolver rewriteClientResolver
+<h3>Known Homeserver List</h3>
+<hr/>
+
+<span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClick="@Execute">Execute</LinkButton></span>
+
+<span>Stats:</span><br/>
+<span>Server count: @entries.Count</span><br/>
+<span>Client server resolution rate (N/O/T): @entries.Count(x => x.HasClientWellKnown)/@entries.Count(x => !string.IsNullOrWhiteSpace(x.LegacyResolutionResult?.Client))/@entries.Count</span>
+<br/>
+<span>Server server resolution rate (N/T): @entries.Count(x => x.HasServerWellKnown)/@entries.Count</span><br/>
+<span>Support resolution rate (N/T): @entries.Count(x => x.HasSupportWellKnown)/@entries.Count</span><br/>
+
+<table class="table-bordered">
+ <thead>
+ <td>Homeserver</td>
+ <td>Client API</td>
+ <td>Server API</td>
+ <td>Has support record</td>
+ </thead>
+ @foreach (var entry in entries) {
+ <tr>
+ <td>@entry.Homeserver</td>
+ <td style="background-color: @GetClientColor(entry)">
+ <span>L: @entry.LegacyResolutionResult?.Client</span><br/>
+ <span>R: @entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver.BaseUrl</span>
+ </td>
+ <td style="background-color: @GetServerColor(entry)">
+ <span>L: @entry.LegacyResolutionResult?.Server</span><br/>
+ <span>R: @entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver</span>
+ </td>
+ <td>@(entry.HasSupportWellKnown ? "Y" : "X")</td>
+ </tr>
+ <tr>
+ <td colspan="6">
+ <details>
+ <pre>@(entry.WellKnownResolutionResult?.ToJson() ?? "null")</pre>
+ </details>
+ </td>
+ </tr>
+ }
+</table>
+
+@code {
+ private List<TableEntry> entries = new();
+
+ [SupplyParameterFromQuery]
+ public string? RoomId { get; set; }
+
+ AuthenticatedHomeserverGeneric? hs { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is null) return;
+
+ if (RoomId is not null) {
+ await Execute();
+ }
+ }
+
+ private class TableEntry {
+ public required string Homeserver { get; set; }
+ public HomeserverResolverService.WellKnownUris? LegacyResolutionResult { get; set; }
+ public WellKnownResolverService.WellKnownRecords? WellKnownResolutionResult { get; set; }
+
+ public bool HasClientWellKnown => WellKnownResolutionResult?.ClientWellKnown is { Content.Homeserver.BaseUrl: { Length: > 0 } };
+ public bool HasServerWellKnown => WellKnownResolutionResult?.ServerWellKnown is { Content.Homeserver.Length: > 0 };
+ public bool HasSupportWellKnown => WellKnownResolutionResult?.SupportWellKnown?.Content is not null and not { SupportPage: null, Contacts: null or { Count: 0 } };
+ }
+
+ private async Task Execute() {
+ var members = await hs!.GetRoom(RoomId!).GetMembersListAsync();
+ var homeservers = members.Select(x => x.StateKey!.Split(':', 2)[1]).Distinct().ToList();
+ var entries = new List<TableEntry>();
+ foreach (var homeserver in homeservers) {
+ var e = new TableEntry() { Homeserver = homeserver };
+ _ = TryResolveLegacy(e);
+ _ = TryFullResolveRewrite(e);
+ entries.Add(e);
+ }
+
+ this.entries = entries;
+ StateHasChanged();
+ }
+
+ private async Task TryResolveLegacy(TableEntry entry) {
+ try {
+ var cTask = legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableServer: false);
+ var sTask = legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableClient: false);
+ entry.LegacyResolutionResult = (await cTask);
+ entry.LegacyResolutionResult.Server = (await sTask).Server;
+ StateHasChanged();
+ }
+ catch { }
+ }
+
+ private async Task TryFullResolveRewrite(TableEntry entry) {
+ try {
+ entry.WellKnownResolutionResult = await rewriteResolver.TryResolveWellKnownRecords(entry.Homeserver);
+ StateHasChanged();
+ }
+ catch { }
+ }
+
+ private string GetClientColor(TableEntry entry) {
+ if (entry.LegacyResolutionResult?.Client == entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl && entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl == null) return "#333333";
+ if (entry.LegacyResolutionResult?.Client == entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl?.TrimEnd('/')) return "#008800";
+ return "#ff0000";
+ }
+
+ private string GetServerColor(TableEntry entry) {
+ if (entry.LegacyResolutionResult?.Server == entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver && entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver == null) return "#333333";
+ if (entry.LegacyResolutionResult?.Server == entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver.TrimEnd('/')) return "#008800";
+ return "#ff0000";
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
index 9c61431..e1b46e2 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
@@ -10,7 +10,13 @@ else {
@if (Homeserver is AuthenticatedHomeserverSynapse) {
<h4>Synapse tools</h4>
<hr/>
- <a href="/HSAdmin/RoomQuery">Query rooms</a>
+ <a href="/HSAdmin/Synapse/RoomQuery">Query rooms</a><br/>
+ <a href="/HSAdmin/Synapse/BlockMedia">Block media</a>
+ }
+ else if (Homeserver is AuthenticatedHomeserverHSE) {
+ <h4>Rory&::LibMatrix.HomeserverEmulator tools</h4>
+ <hr/>
+ <a href="/HSAdmin/HSE/ManageExternalProfiles">Manage external profiles</a>
}
else {
<p>Homeserver type @Homeserver.GetType().Name does not have any administration tools in RMU.</p>
@@ -24,7 +30,7 @@ else {
public ServerVersionResponse? ServerVersionResponse { get; set; }
protected override async Task OnInitializedAsync() {
- Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
ServerVersionResponse = await (Homeserver.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null));
await base.OnInitializedAsync();
diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
new file mode 100644
index 0000000..87600c6
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
@@ -0,0 +1,43 @@
+@page "/HSAdmin/HSE/ManageExternalProfiles"
+@using ArcaneLibs.Extensions
+@using LibMatrix.Responses
+<h3>Manage external profiles</h3>
+
+<LinkButton OnClick="AddAllLocalProfiles">Add local sessions</LinkButton>
+
+@foreach(var p in ExternalProfiles)
+{
+ <h4>@p.Key</h4>
+ <pre>@p.Value.ToJson(indent: true)</pre>
+}
+
+@code {
+ public AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+ private Dictionary<string, LoginResponse> ExternalProfiles = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (Homeserver is null) return;
+ await LoadProfiles();
+ await base.OnInitializedAsync();
+ }
+
+ private async Task LoadProfiles() {
+ if(Homeserver is AuthenticatedHomeserverHSE hse)
+ {
+ ExternalProfiles = await hse.GetExternalProfilesAsync();
+ }
+ StateHasChanged();
+ }
+
+ private async Task AddAllLocalProfiles() {
+ if(Homeserver is AuthenticatedHomeserverHSE hse) {
+ var sessions = await sessionStore.GetAllSessions();
+ foreach(var session in sessions) {
+ await hse.SetExternalProfile(session.Value.Auth.UserId, session.Value.Auth);
+ }
+ await LoadProfiles();
+ }
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor
deleted file mode 100644
index 11df261..0000000
--- a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor
+++ /dev/null
@@ -1,201 +0,0 @@
-@page "/HSAdmin/RoomQuery"
-@using LibMatrix.Responses.Admin
-@using LibMatrix.Filters
-@using ArcaneLibs.Extensions
-
-<h3>Homeserver Administration - Room Query</h3>
-
-<label>Search name: </label>
-<InputText @bind-Value="SearchTerm"/><br/>
-<label>Order by: </label>
-<select @bind="OrderBy">
- @foreach (var item in validOrderBy) {
- <option value="@item.Key">@item.Value</option>
- }
-</select><br/>
-<label>Ascending: </label>
-<InputCheckbox @bind-Value="Ascending"/><br/>
-<details>
- <summary>
- <span>Local filtering (slow)</span>
-
- </summary>
- <div style="margin-left: 8px; margin-bottom: 8px;">
- <u style="display: block;">String contains</u>
- <span class="tile tile280">Room ID: <FancyTextBox @bind-Value="@Filter.RoomIdContains"></FancyTextBox></span>
- <span class="tile tile280">Room name: <FancyTextBox @bind-Value="@Filter.NameContains"></FancyTextBox></span>
- <span class="tile tile280">Canonical alias: <FancyTextBox @bind-Value="@Filter.CanonicalAliasContains"></FancyTextBox></span>
- <span class="tile tile280">Creator: <FancyTextBox @bind-Value="@Filter.CreatorContains"></FancyTextBox></span>
- <span class="tile tile280">Room version: <FancyTextBox @bind-Value="@Filter.VersionContains"></FancyTextBox></span>
- <span class="tile tile280">Encryption algorithm: <FancyTextBox @bind-Value="@Filter.EncryptionContains"></FancyTextBox></span>
- <span class="tile tile280">Join rules: <FancyTextBox @bind-Value="@Filter.JoinRulesContains"></FancyTextBox></span>
- <span class="tile tile280">Guest access: <FancyTextBox @bind-Value="@Filter.GuestAccessContains"></FancyTextBox></span>
- <span class="tile tile280">History visibility: <FancyTextBox @bind-Value="@Filter.HistoryVisibilityContains"></FancyTextBox></span>
-
- <u style="display: block;">Optional checks</u>
- <span class="tile tile150">
- <InputCheckbox @bind-Value="@Filter.CheckFederation"></InputCheckbox> Is federated:
- @if (Filter.CheckFederation) {
- <InputCheckbox @bind-Value="@Filter.Federatable"></InputCheckbox>
- }
- </span>
- <span class="tile tile150">
- <InputCheckbox @bind-Value="@Filter.CheckPublic"></InputCheckbox> Is public:
- @if (Filter.CheckPublic) {
- <InputCheckbox @bind-Value="@Filter.Public"></InputCheckbox>
- }
- </span>
-
- <u style="display: block;">Ranges</u>
- <span class="tile center-children">
- <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsGreaterThan"></InputNumber><span class="range-sep">state events</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsLessThan"></InputNumber>
- </span>
- <span class="tile center-children">
- <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersGreaterThan"></InputNumber><span class="range-sep">members</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersLessThan"></InputNumber>
- </span>
- <span class="tile center-children">
- <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembersGreaterThan"></InputNumber><span class="range-sep">local members</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembersLessThan"></InputNumber>
- </span>
- </div>
-</details>
-<button class="btn btn-primary" @onclick="Search">Search</button>
-<br/>
-
-@if (Results.Count > 0) {
- <p>Found @Results.Count rooms</p>
- <details>
- <summary>TSV data (copy/paste)</summary>
- <pre style="font-size: 0.6em;">
- <table>
- @foreach (var res in Results) {
- <tr>
- <td style="padding: 8px;">@res.RoomId@("\t")</td>
- <td style="padding: 8px;">@res.CanonicalAlias@("\t")</td>
- <td style="padding: 8px;">@res.Creator@("\t")</td>
- <td style="padding: 8px;">@res.Name</td>
- </tr>
- }
- </table>
- </pre>
- </details>
-}
-
-@foreach (var res in Results) {
- <div style="background-color: #ffffff11; border-radius: 0.5em; display: block; margin-top: 4px; padding: 4px;">
- @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@
- <p>
- @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) {
- <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span>
- <br/>
- }
- else {
- <span>@res.RoomId (@res.Name)</span>
- <br/>
- }
- @if (!string.IsNullOrWhiteSpace(res.Creator)) {
- @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@
- <span>Created by @res.Creator</span>
- <br/>
- }
- </p>
- <span>@res.StateEvents state events</span><br/>
- <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span>
- <details>
- <summary>Full result data</summary>
- <pre>@res.ToJson(ignoreNull: true)</pre>
- </details>
- </div>
-}
-
-<style>
- .int-input {
- width: 128px;
- }
- .tile {
- display: inline-block;
- padding: 4px;
- border: 1px solid #ffffff22;
- }
- .tile280 {
- min-width: 280px;
- }
- .tile150 {
- min-width: 150px;
- }
- .range-sep {
- display: inline-block;
- padding: 4px;
- width: 150px;
- }
- .range-sep::before {
- content: "@("<") ";
- }
- .range-sep::after {
- content: " @("<")";
- }
- .center-children {
- text-align: center;
- }
-</style>
-
-@code {
-
- [Parameter]
- [SupplyParameterFromQuery(Name = "order_by")]
- public string? OrderBy { get; set; }
-
- [Parameter]
- [SupplyParameterFromQuery(Name = "name_search")]
- public string SearchTerm { get; set; }
-
- [Parameter]
- [SupplyParameterFromQuery(Name = "ascending")]
- public bool Ascending { get; set; }
-
- public List<AdminRoomListingResult.AdminRoomListingResultRoom> Results { get; set; } = new();
-
- private string Status { get; set; }
-
- public LocalRoomQueryFilter Filter { get; set; } = new();
-
- protected override Task OnParametersSetAsync() {
- if (Ascending == null)
- Ascending = true;
- OrderBy ??= "name";
- return Task.CompletedTask;
- }
-
- private async Task Search() {
- Results.Clear();
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
- if (hs is AuthenticatedHomeserverSynapse synapse) {
- var searchRooms = synapse.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator();
- while (await searchRooms.MoveNextAsync()) {
- var room = searchRooms.Current;
- Console.WriteLine("Hit: " + room.ToJson(false));
- Results.Add(room);
- if (Results.Count % 10 == 0)
- StateHasChanged();
- }
- }
-
- StateHasChanged();
- }
-
- private readonly Dictionary<string, string> validOrderBy = new() {
- { "name", "Room name" },
- { "canonical_alias", "Main alias address" },
- { "joined_members", "Number of members (reversed)" },
- { "joined_local_members", "Number of local members (reversed)" },
- { "version", "Room version" },
- { "creator", "Creator of the room" },
- { "encryption", "End-to-end encryption algorithm" },
- { "federatable", "Is room federated" },
- { "public", "Visibility in room list" },
- { "join_rules", "Join rules" },
- { "guest_access", "Guest access" },
- { "history_visibility", "Visibility of history" },
- { "state_events", "Number of state events" }
- };
-
-}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor
new file mode 100644
index 0000000..d855cba
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor
@@ -0,0 +1,29 @@
+@page "/HSAdmin/Synapse/BackgroundJobs"
+@using ArcaneLibs.Extensions
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses
+
+<h3>Homeserver Administration - Background jobs</h3>
+<pre>@BackgroundJobStatus?.ToJson(ignoreNull: true)</pre>
+
+@code {
+ private AuthenticatedHomeserverSynapse? Homeserver { get; set; }
+ private SynapseAdminBackgroundUpdateStatusResponse? BackgroundJobStatus { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) as AuthenticatedHomeserverSynapse;
+ if (hs is null) return;
+ Homeserver = hs;
+
+ while (true) {
+ try {
+ BackgroundJobStatus = await hs.Admin.GetBackgroundUpdatesStatusAsync();
+ StateHasChanged();
+ await Task.Delay(1000);
+ }
+ catch (Exception ex) {
+ Console.WriteLine(ex);
+ }
+ }
+ }
+
+}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
new file mode 100644
index 0000000..d07ff08
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
@@ -0,0 +1,191 @@
+@page "/HSAdmin/Synapse/BlockMedia"
+@using System.Text.Json
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec
+
+<h3>Homeserver Administration - Block media</h3>
+@if (Homeserver is not null) {
+ <label>Event URL: </label>
+ <FancyTextBox @bind-Value="EventUrl"/>
+ <br/>
+ <label>Event JSON: </label>
+ <details>
+ <summary>@(string.IsNullOrEmpty(EventJson) ? "" : "{ ... }")</summary>
+ <FancyTextBox Multiline="true" @bind-Value="EventJson"/>
+ </details>
+ <br/>
+ <label>MXC URI: </label>
+ <FancyTextBox @bind-Value="MxcUrl"/>
+ <br/>
+ <label>Room ID: </label>
+ <FancyTextBox @bind-Value="RoomId"/>
+ <br/>
+ <pre>@MxcUri?.ToJson(ignoreNull: true)</pre>
+
+ @if (Event is not null) {
+ <LinkButton OnClick="@RedactAllEvents">Redact all messages</LinkButton>
+ }
+
+ @if (Event?.Sender?.Split(':', 2)[1] == Homeserver?.ServerName) {
+ <p>User is a local user!</p>
+ <LinkButton OnClick="@DeactivateUser">Deactivate User</LinkButton>
+ <LinkButton OnClick="@QuarantineMediaByUser">Quarantine all media</LinkButton>
+ }
+}
+
+<style>
+ .int-input {
+ width: 128px;
+ }
+
+ .tile {
+ display: inline-block;
+ padding: 4px;
+ border: 1px solid #ffffff22;
+ }
+
+ .tile280 {
+ min-width: 280px;
+ }
+
+ .tile150 {
+ min-width: 150px;
+ }
+
+ .range-sep {
+ display: inline-block;
+ padding: 4px;
+ width: 150px;
+ }
+
+ .range-sep::before {
+ content: "@("<") ";
+ }
+
+ .range-sep::after {
+ content: " @("<")";
+ }
+
+ .center-children {
+ text-align: center;
+ }
+</style>
+
+@code {
+
+ private AuthenticatedHomeserverSynapse? Homeserver { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) as AuthenticatedHomeserverSynapse;
+ if (hs is null) return;
+ Homeserver = hs;
+
+ if (!string.IsNullOrWhiteSpace(EventUrl)) {
+ _ = ExpandEventUrl();
+ }
+ }
+
+ [SupplyParameterFromQuery]
+ public string? EventUrl {
+ get;
+ set {
+ field = value?.Split('?')[0];
+ _ = ExpandEventUrl();
+ }
+ }
+
+ private StateEventResponse? Event { get; set; }
+
+ private string? EventJson {
+ get;
+ set {
+ field = value;
+ _ = ExpandEventJson();
+ }
+ }
+
+ private string? MxcUrl {
+ get;
+ set {
+ field = value;
+ _ = ExpandMxcUri();
+ }
+ }
+
+ private MxcUri? MxcUri { get; set; }
+
+ private string? RoomId {
+ get => Event?.RoomId ?? field;
+ set;
+ }
+
+ private async Task ExpandEventUrl() {
+ Console.WriteLine("Expanding event URL...");
+ if (!string.IsNullOrWhiteSpace(EventUrl)) {
+ Console.WriteLine("Parsing event URL...");
+ var data = ParseEventUrl(EventUrl);
+ Console.WriteLine($"Room: {data.room}, Event: {data.eventId}");
+ RoomId = data.room;
+ var room = Homeserver.GetRoom(data.room);
+ var eventResponse = await room.GetEventAsync(data.eventId);
+ eventResponse.RoomId ??= data.room;
+ EventJson = eventResponse?.ToJson() ?? "null";
+ }
+
+ StateHasChanged();
+ }
+
+ private async Task ExpandEventJson() {
+ Console.WriteLine("Expanding event JSON...");
+ if (!string.IsNullOrWhiteSpace(EventJson)) {
+ Event = JsonSerializer.Deserialize<StateEventResponse>(EventJson);
+ MxcUrl = Event?.ContentAs<RoomMessageEventContent>()?.Url;
+ Console.WriteLine($"MXC URL: {MxcUrl}");
+
+ var possiblyRelated = await Homeserver.Admin.GetRoomMediaAsync(Event!.RoomId!);
+ }
+
+ StateHasChanged();
+ }
+
+ private async Task ExpandMxcUri() {
+ Console.WriteLine("Expanding MXC URI...");
+ if (!string.IsNullOrWhiteSpace(MxcUrl)) {
+ MxcUri = MxcUrl;
+ }
+
+ StateHasChanged();
+ }
+
+ private (string room, string eventId) ParseEventUrl(string url) {
+ var parts = url.Split('/');
+ Console.WriteLine($"Parts: {string.Join(", ", parts)}");
+ return (parts[4].UrlDecode(), parts[5].Split('?')[0].UrlDecode());
+ }
+
+#region Local user
+
+ private async Task DeactivateUser() {
+ await Homeserver.Admin.DeactivateUserAsync(Event.Sender, true);
+ }
+
+ private async Task QuarantineMediaByUser() {
+ if (Event is null) return;
+ var media = Homeserver.Admin.GetUserMediaEnumerableAsync(Event?.Sender!);
+ await foreach (var m in media) {
+ if (m is not null) {
+ // await Homeserver.Admin.QuarantineMedia(m);
+ // await Homeserver.Admin.DeleteMedia(m);
+ }
+ }
+ }
+
+#endregion
+
+ private async Task RedactAllEvents() {
+ if (Event is null) return;
+ await Homeserver!.Admin.DeleteAllMessages(Event.Sender!);
+ }
+
+}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css
index e69de29..e69de29 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor.css
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor
new file mode 100644
index 0000000..d598994
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor
@@ -0,0 +1,5 @@
+<h3>SynapseRoomShutdownWindow</h3>
+
+@code {
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
new file mode 100644
index 0000000..d5daf75
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -0,0 +1,113 @@
+@using LibMatrix.Homeservers.Extensions.NamedCaches
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests
+
+@if (string.IsNullOrWhiteSpace(Context.DeleteId)) {
+ <b>Media options</b>
+ <br/>
+ <hr/>
+ <span>Quarantine local media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalMedia"/>
+ <br/>
+ <span>Quarantine remote media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineRemoteMedia"/>
+ <br/>
+ <span>Delete remote media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteRemoteMedia"/>
+ <br/>
+
+ <b>User options</b>
+ <br/>
+ <hr/>
+ <span>Suspend local users: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.SuspendLocalUsers"></InputCheckbox>
+ <br/>
+ <span>Quarantine <b>ALL</b> local user media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalUserMedia"></InputCheckbox>
+ <br/>
+ <span>Delete <b>ALL</b> local user media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteLocalUserMedia"></InputCheckbox>
+ <br/>
+
+ <b>Room deletion options</b>
+ <br/>
+ <hr/>
+ <span>Block room: </span>
+ <InputCheckbox @bind-Value="@Context.DeleteRequest.Block"/>
+ <br/>
+ <span>Purge room: </span>
+ <InputCheckbox @bind-Value="@Context.DeleteRequest.Purge"/>
+ <br/>
+ <span>Force purge room (unsafe): </span>
+ <InputCheckbox @bind-Value="@Context.DeleteRequest.ForcePurge"></InputCheckbox>
+ <br/>
+ <span>Warning room User ID (optional): </span>
+ <FancyTextBox @bind-Value="@Context.DeleteRequest.NewRoomUserId"/>
+ <br/>
+ @if (!string.IsNullOrWhiteSpace(Context.DeleteRequest.NewRoomUserId)) {
+ <span>Warning room name: </span>
+ <FancyTextBox @bind-Value="@Context.DeleteRequest.RoomName"/>
+ <br/>
+ <span>Warning room message (plaintext): </span>
+ <FancyTextBox Multiline="true" @bind-Value="@Context.DeleteRequest.Message"/>
+ <br/>
+ }
+
+ <LinkButton OnClick="@DeleteRoom">Execute</LinkButton>
+}
+
+@code {
+
+ [Parameter]
+ public required RoomShutdownContext Context { get; set; }
+
+ [Parameter]
+ public required AuthenticatedHomeserverSynapse Homeserver { get; set; }
+
+ private NamedCache<RoomShutdownContext> TaskMap { get; set; } = null!;
+
+ protected override async Task OnInitializedAsync() {
+ TaskMap = new NamedCache<RoomShutdownContext>(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks");
+ }
+
+ public class RoomShutdownContext {
+ public required string RoomId { get; set; }
+ public string? DeleteId { get; set; }
+ public ExtraDeleteOptions ExtraOptions { get; set; } = new();
+
+ public SynapseAdminRoomDeleteRequest DeleteRequest { get; set; } = new() {
+ Block = true,
+ Purge = true,
+ ForcePurge = false
+ };
+
+ public class ExtraDeleteOptions {
+ // room options
+ public bool QuarantineLocalMedia { get; set; }
+ public bool QuarantineRemoteMedia { get; set; }
+
+ public bool DeleteRemoteMedia { get; set; }
+
+ // user options
+ public bool SuspendLocalUsers { get; set; }
+ public bool QuarantineLocalUserMedia { get; set; }
+ public bool DeleteLocalUserMedia { get; set; }
+ }
+ }
+
+ public async Task OnComplete() {
+ await OnCompleteLock.WaitAsync();
+ try {
+ await TaskMap.RemoveValueAsync(Context.DeleteId!);
+ }
+ finally {
+ OnCompleteLock.Release();
+ }
+ }
+
+ public async Task DeleteRoom() {
+ await TaskMap.SetValueAsync(Context.RoomId, Context);
+ }
+
+ private static readonly SemaphoreSlim OnCompleteLock = new(1, 1);
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
new file mode 100644
index 0000000..79e7357
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -0,0 +1,420 @@
+@page "/HSAdmin/Synapse/RoomQuery"
+@using Microsoft.AspNetCore.WebUtilities
+@using ArcaneLibs.Extensions
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses
+@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components
+
+<h3>Homeserver Administration - Room Query</h3>
+
+<label>Search name: </label>
+<InputText @bind-Value="SearchTerm"/><br/>
+<label>Order by: </label>
+<select @bind="OrderBy">
+ @foreach (var item in validOrderBy) {
+ <option value="@item.Key">@item.Value</option>
+ }
+</select><br/>
+<label>Ascending: </label>
+<InputCheckbox @bind-Value="Ascending"/><br/>
+<details>
+ <summary>
+ <span>Local filtering (slow)</span>
+
+ </summary>
+ <div style="margin-left: 8px; margin-bottom: 8px;">
+ <u style="display: block;">String contains</u>
+ <span class="tile tile280">Room ID: <FancyTextBox @bind-Value="@Filter.RoomIdContains"></FancyTextBox></span>
+ <span class="tile tile280">Room name: <FancyTextBox @bind-Value="@Filter.NameContains"></FancyTextBox></span>
+ <span class="tile tile280">Canonical alias: <FancyTextBox @bind-Value="@Filter.CanonicalAliasContains"></FancyTextBox></span>
+ <span class="tile tile280">Creator: <FancyTextBox @bind-Value="@Filter.CreatorContains"></FancyTextBox></span>
+ <span class="tile tile280">Room version: <FancyTextBox @bind-Value="@Filter.VersionContains"></FancyTextBox></span>
+ <span class="tile tile280">Encryption algorithm: <FancyTextBox @bind-Value="@Filter.EncryptionContains"></FancyTextBox></span>
+ <span class="tile tile280">Join rules: <FancyTextBox @bind-Value="@Filter.JoinRulesContains"></FancyTextBox></span>
+ <span class="tile tile280">Guest access: <FancyTextBox @bind-Value="@Filter.GuestAccessContains"></FancyTextBox></span>
+ <span class="tile tile280">History visibility: <FancyTextBox @bind-Value="@Filter.HistoryVisibilityContains"></FancyTextBox></span>
+
+ <u style="display: block;">Optional checks</u>
+ <span class="tile tile150">
+ <InputCheckbox @bind-Value="@Filter.CheckFederation"></InputCheckbox> Is federated:
+ @if (Filter.CheckFederation) {
+ <InputCheckbox @bind-Value="@Filter.Federatable"></InputCheckbox>
+ }
+ </span>
+ <span class="tile tile150">
+ <InputCheckbox @bind-Value="@Filter.CheckPublic"></InputCheckbox> Is public:
+ @if (Filter.CheckPublic) {
+ <InputCheckbox @bind-Value="@Filter.Public"></InputCheckbox>
+ }
+ </span>
+
+ <u style="display: block;">Ranges</u>
+ <span class="tile center-children">
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsGreaterThan"></InputNumber><span class="range-sep">state events</span><InputNumber
+ max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsLessThan"></InputNumber>
+ </span>
+ <span class="tile center-children">
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersGreaterThan"></InputNumber><span class="range-sep">members</span><InputNumber
+ max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersLessThan"></InputNumber>
+ </span>
+ <span class="tile center-children">
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembersGreaterThan"></InputNumber><span
+ class="range-sep">local members</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int"
+ @bind-Value="@Filter.JoinedLocalMembersLessThan"></InputNumber>
+ </span>
+ </div>
+</details>
+<button class="btn btn-primary" @onclick="Search">Search</button>
+<br/>
+
+@if (Results.Count > 0) {
+ <p>Found @Results.Count rooms</p>
+ <details>
+ <summary>TSV data (copy/paste)</summary>
+ <pre style="font-size: 0.6em;">
+ <table>
+ @foreach (var res in Results) {
+ <tr>
+ <td style="padding: 8px;">@res.RoomId@("\t")</td>
+ <td style="padding: 8px;">@res.CanonicalAlias@("\t")</td>
+ <td style="padding: 8px;">@res.Creator@("\t")</td>
+ <td style="padding: 8px;">@res.Name</td>
+ </tr>
+ }
+ </table>
+ </pre>
+ </details>
+}
+
+@foreach (var res in Results) {
+ <div style="background-color: #ffffff11; border-radius: 0.5em; display: block; margin-top: 4px; padding: 4px;">
+ @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@
+ <p>
+ @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) {
+ <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span>
+ <br/>
+ }
+ else {
+ <span>@res.RoomId (@res.Name)</span>
+ <br/>
+ }
+ @if (!string.IsNullOrWhiteSpace(res.Creator)) {
+ @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@
+ <span>Created by @res.Creator</span>
+ <br/>
+ }
+ </p>
+ <p>
+ <LinkButton OnClick="@(() => {
+ DeleteRequests.Add(res.RoomId, new() {
+ RoomId = res.RoomId,
+ DeleteRequest = new() {
+ Block = true,
+ Purge = true,
+ ForcePurge = false
+ }
+ });
+
+ return Task.CompletedTask;
+ })">Delete room
+ </LinkButton>
+ </p>
+ <span>@res.StateEvents state events</span><br/>
+ @if (res.LocalMembers is null) {
+ <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span>
+ }
+ else {
+ <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server: @(string.Join(", ", res.LocalMembers))</span>
+ }
+ <details>
+ <summary>Full result data</summary>
+ <pre>@res.ToJson(ignoreNull: true)</pre>
+ </details>
+ </div>
+}
+@* *@
+@* @if (DeleteRequest.HasValue) { *@
+@* <ModalWindow MinWidth="600" Title="@("Delete " + DeleteRequest.Value.RoomId)" OnCloseClicked="@(() => { DeleteRequest = null; })"> *@
+@* *@
+@* </ModalWindow> *@
+@* } *@
+
+@* @foreach (var (roomId, status) in DeleteStatuses) { *@
+@* <ModalWindow Title="@("Delete status for " + roomId)" MinWidth="600"> *@
+@* <pre>@status.ToJson()</pre> *@
+@* </ModalWindow> *@
+@* } *@
+
+@foreach(var (roomId, deleteRequest) in DeleteRequests) {
+ <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/>
+}
+
+<style>
+ .int-input {
+ width: 128px;
+ }
+
+ .tile {
+ display: inline-block;
+ padding: 4px;
+ border: 1px solid #ffffff22;
+ }
+
+ .tile280 {
+ min-width: 280px;
+ }
+
+ .tile150 {
+ min-width: 150px;
+ }
+
+ .range-sep {
+ display: inline-block;
+ padding: 4px;
+ width: 150px;
+ }
+
+ .range-sep::before {
+ content: "@("<") ";
+ }
+
+ .range-sep::after {
+ content: " @("<")";
+ }
+
+ .center-children {
+ text-align: center;
+ }
+</style>
+
+@code {
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "order_by")]
+ public string? OrderBy { get; set; }
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "name_search")]
+ public string SearchTerm { get; set; }
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "ascending")]
+ public bool Ascending { get; set; }
+
+ private List<RoomInfo> Results { get; set; } = new();
+
+ private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!;
+
+ private string Status { get; set; }
+
+ public SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new();
+
+ private Dictionary<string, SynapseRoomShutdownWindowContent.RoomShutdownContext> DeleteRequests { get; set; } = [];
+
+ // private Dictionary<string, SynapseAdminRoomDeleteStatus> DeleteStatuses { get; set; } = new();
+
+ protected override Task OnParametersSetAsync() {
+ if (Ascending == null)
+ Ascending = true;
+ OrderBy ??= "name";
+
+ var execute = false;
+
+ foreach (var (key, value) in QueryHelpers.ParseQuery(new Uri(NavigationManager.Uri).Query)) {
+ switch (key) {
+ case "RoomIdContains":
+ Filter.RoomIdContains = value[0]!;
+ break;
+ case "NameContains":
+ Filter.NameContains = value[0]!;
+ break;
+ case "CanonicalAliasContains":
+ Filter.CanonicalAliasContains = value[0]!;
+ break;
+ case "VersionContains":
+ Filter.VersionContains = value[0]!;
+ break;
+ case "CreatorContains":
+ Filter.CreatorContains = value[0]!;
+ break;
+ case "EncryptionContains":
+ Filter.EncryptionContains = value[0]!;
+ break;
+ case "JoinRulesContains":
+ Filter.JoinRulesContains = value[0]!;
+ break;
+ case "GuestAccessContains":
+ Filter.GuestAccessContains = value[0]!;
+ break;
+ case "HistoryVisibilityContains":
+ Filter.HistoryVisibilityContains = value[0]!;
+ break;
+ case "Federatable":
+ Filter.Federatable = bool.Parse(value[0]!);
+ Filter.CheckFederation = true;
+ break;
+ case "Public":
+ Filter.Public = value[0] == "true";
+ Filter.CheckPublic = true;
+ break;
+ case "JoinedMembersGreaterThan":
+ Filter.JoinedMembersGreaterThan = int.Parse(value[0]!);
+ break;
+ case "JoinedMembersLessThan":
+ Filter.JoinedMembersLessThan = int.Parse(value[0]!);
+ break;
+ case "JoinedLocalMembersGreaterThan":
+ Filter.JoinedLocalMembersGreaterThan = int.Parse(value[0]!);
+ break;
+ case "JoinedLocalMembersLessThan":
+ Filter.JoinedLocalMembersLessThan = int.Parse(value[0]!);
+ break;
+ case "StateEventsGreaterThan":
+ Filter.StateEventsGreaterThan = int.Parse(value[0]!);
+ break;
+ case "StateEventsLessThan":
+ Filter.StateEventsLessThan = int.Parse(value[0]!);
+ break;
+ case "Execute":
+ execute = true;
+ break;
+ default:
+ Console.WriteLine($"Unknown query parameter: {key}");
+ break;
+ }
+ }
+
+ if (execute)
+ _ = Search();
+
+ return Task.CompletedTask;
+ }
+
+ private async Task Search() {
+ Results.Clear();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is AuthenticatedHomeserverSynapse synapse) {
+ Homeserver = synapse;
+ var searchRooms = synapse.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator();
+ while (await searchRooms.MoveNextAsync()) {
+ var room = searchRooms.Current;
+
+ if (Results.Count < 100)
+ Console.WriteLine("Hit: " + room.ToJson(false));
+
+ var roomInfo = new RoomInfo {
+ RoomId = room.RoomId,
+ Name = room.Name,
+ CanonicalAlias = room.CanonicalAlias,
+ Creator = room.Creator,
+ Version = room.Version,
+ Encryption = room.Encryption,
+ Federatable = room.Federatable,
+ Public = room.Public,
+ JoinRules = room.JoinRules,
+ GuestAccess = room.GuestAccess,
+ HistoryVisibility = room.HistoryVisibility,
+ StateEvents = room.StateEvents,
+ JoinedMembers = room.JoinedMembers,
+ JoinedLocalMembers = room.JoinedLocalMembers
+ };
+
+ Results.Add(roomInfo);
+
+ if (room.JoinedLocalMembers is > 0 and < 100)
+ roomInfo.LocalMembers = (await synapse.Admin.GetRoomMembersAsync(room.RoomId)).Members.Where(x => x.EndsWith(":" + synapse.ServerName)).ToList();
+
+ if (Results.Count < 200 || Results.Count % 1000 == 0) {
+ StateHasChanged();
+ await Task.Yield();
+ }
+ }
+ }
+
+ StateHasChanged();
+ }
+ //
+ // private async Task DeleteRoom() {
+ // if (DeleteRequest is { } deleteRequest) {
+ // var media = await Homeserver.Admin.GetRoomMediaAsync(deleteRequest.RoomId);
+ // if (deleteRequest.DeleteRequest.QuarantineRemoteMedia) {
+ // foreach (var remoteMedia in media.Remote) {
+ // await Homeserver.Admin.QuarantineMediaById(remoteMedia);
+ // }
+ // }
+ //
+ // if (deleteRequest.DeleteRequest.DeleteRemoteMedia) {
+ // foreach (var remoteMedia in media.Remote) {
+ // await Homeserver.Admin.DeleteMediaById(remoteMedia);
+ // }
+ // }
+ // else if (deleteRequest.DeleteRequest.QuarantineLocalMedia) {
+ // foreach (var localMedia in media.Local) {
+ // await Homeserver.Admin.QuarantineMediaById(localMedia);
+ // }
+ // }
+ //
+ // var deleteId = await Homeserver.Admin.DeleteRoom(deleteRequest.RoomId, deleteRequest.DeleteRequest, waitForCompletion: false);
+ // DeleteRequest = null;
+ // List<string> alreadyCleanedUsers = [];
+ // while (true) {
+ // var status = await Homeserver.Admin.GetRoomDeleteStatus(deleteId.DeleteId);
+ // DeleteStatuses[deleteRequest.RoomId] = status;
+ // StateHasChanged();
+ // await Task.Delay(5000);
+ // if (status.Status == "complete") {
+ // DeleteStatuses.Remove(deleteRequest.RoomId);
+ // StateHasChanged();
+ // break;
+ // }
+ //
+ // if (status.Status == "failed") {
+ // deleteId = await Homeserver.Admin.DeleteRoom(deleteRequest.RoomId, deleteRequest.DeleteRequest, waitForCompletion: false);
+ // }
+ //
+ // var newCleanedUsers = status.ShutdownRoom?.KickedUsers?.Except(alreadyCleanedUsers).ToList();
+ // if (newCleanedUsers is not null) {
+ // alreadyCleanedUsers.AddRange(newCleanedUsers);
+ // foreach (var user in newCleanedUsers) {
+ // if (deleteRequest.DeleteRequest.SuspendLocalUsers) {
+ // // await Homeserver.Admin.(user);
+ // }
+ //
+ // if (deleteRequest.DeleteRequest.QuarantineLocalUserMedia) {
+ // await Homeserver.Admin.QuarantineMediaByUserId(user);
+ // }
+ //
+ // if (deleteRequest.DeleteRequest.DeleteLocalUserMedia) {
+ // var userMedia = Homeserver.Admin.GetUserMediaEnumerableAsync(user);
+ // await foreach (var mediaEntry in userMedia) {
+ // await Homeserver.Admin.DeleteMediaById(mediaEntry.MediaId);
+ // }
+ // }
+ // }
+ // }
+ // }
+ // }
+ // }
+
+ private readonly Dictionary<string, string> validOrderBy = new() {
+ { "name", "Room name" },
+ { "canonical_alias", "Main alias address" },
+ { "joined_members", "Number of members (reversed)" },
+ { "joined_local_members", "Number of local members (reversed)" },
+ { "version", "Room version" },
+ { "creator", "Creator of the room" },
+ { "encryption", "End-to-end encryption algorithm" },
+ { "federatable", "Is room federated" },
+ { "public", "Visibility in room list" },
+ { "join_rules", "Join rules" },
+ { "guest_access", "Guest access" },
+ { "history_visibility", "Visibility of history" },
+ { "state_events", "Number of state events" }
+ };
+
+ private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom {
+ public List<string>? LocalMembers { get; set; }
+ }
+
+}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css
diff --git a/MatrixUtils.Web/Pages/HSEInit.razor b/MatrixUtils.Web/Pages/HSEInit.razor
index cabc671..1eb556a 100644
--- a/MatrixUtils.Web/Pages/HSEInit.razor
+++ b/MatrixUtils.Web/Pages/HSEInit.razor
@@ -19,7 +19,7 @@
async Task<UserAuth?> Login() {
try {
- var result = new UserAuth(await hsProvider.Login("http://localhost:5298", $"{Guid.NewGuid().ToString()}", ""));
+ var result = new UserAuth(await HsProvider.Login("http://localhost:5298", $"{Guid.NewGuid().ToString()}", ""));
if (result == null) {
Console.WriteLine($"Failed to login!");
return null;
diff --git a/MatrixUtils.Web/Pages/Index.razor b/MatrixUtils.Web/Pages/Index.razor
index a7619ae..cefd7d5 100644
--- a/MatrixUtils.Web/Pages/Index.razor
+++ b/MatrixUtils.Web/Pages/Index.razor
@@ -22,20 +22,29 @@ Small collection of tools to do not-so-everyday things.
<form>
<table>
@foreach (var session in _sessions.OrderByDescending(x => x.UserInfo.RoomCount)) {
- var _auth = session.UserAuth;
+ var auth = session.Auth;
<tr class="user-entry">
<td>
- <img class="avatar" src="@session.UserInfo.AvatarUrl" crossorigin="anonymous"/>
+ @if (!string.IsNullOrWhiteSpace(@session.UserInfo?.AvatarUrl)) {
+ // Console.WriteLine($"Rendering {session.UserInfo.AvatarUrl} with homeserver {session.Homeserver}");
+ <MxcAvatar Homeserver="@session.Homeserver" MxcUri="@session.UserInfo.AvatarUrl" Circular="true" Size="4" SizeUnit="em"/>
+ }
+ else {
+ <img class="avatar" src="@_identiconGenerator.GenerateAsDataUri(session.Homeserver.WhoAmI.UserId)"/>
+ }
+ @* <img class="avatar" src="@session.UserInfo.AvatarUrl" crossorigin="anonymous"/> *@
</td>
<td class="user-info">
<p>
- <input type="radio" name="csa" checked="@(_currentSession.AccessToken == _auth.AccessToken)" @onclick="@(() => SwitchSession(_auth))" style="text-decoration-line: unset;"/>
- <b>@session.UserInfo.DisplayName</b> on <b>@_auth.Homeserver</b><br/>
+ <input type="radio" name="csa" checked="@(_currentSession.Auth.AccessToken == auth.AccessToken)" @onclick="@(() => SwitchSession(session.SessionId))"
+ style="text-decoration-line: unset;"/>
+ <b>@session.UserInfo.DisplayName</b> on <b>@auth.Homeserver</b><br/>
</p>
<span style="display: inline-block; width: 128px;">@session.UserInfo.RoomCount rooms</span>
- <a style="color: #888888" href="@("/ServerInfo/" + session.Homeserver?.ServerName + "/")">@session.ServerVersion?.Server.Name @session.ServerVersion?.Server.Version</a>
- @if (_auth.Proxy != null) {
- <span class="badge badge-info"> (proxied via @_auth.Proxy)</span>
+ <a style="color: #888888"
+ href="@("/ServerInfo/" + session.Homeserver?.ServerName + "/")">@session.ServerVersion?.Server.Name @session.ServerVersion?.Server.Version</a>
+ @if (auth.Proxy != null) {
+ <span class="badge badge-info"> (proxied via @auth.Proxy)</span>
}
else {
<p>Not proxied</p>
@@ -44,13 +53,14 @@ Small collection of tools to do not-so-everyday things.
<p>T=@session.Homeserver.GetType().FullName</p>
<p>D=@session.Homeserver.WhoAmI.DeviceId</p>
<p>U=@session.Homeserver.WhoAmI.UserId</p>
+ <p>S=@session.Homeserver.WhoAmI.UserId</p>
}
</td>
<td>
<p>
- <LinkButton OnClick="@(() => ManageUser(_auth))">Manage</LinkButton>
- <LinkButton OnClick="@(() => RemoveUser(_auth))">Remove</LinkButton>
- <LinkButton OnClick="@(() => RemoveUser(_auth, true))">Log out</LinkButton>
+ <LinkButton OnClick="@(() => ManageUser(session.SessionId))">Manage</LinkButton>
+ <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
+ <LinkButton OnClick="@(() => RemoveUser(session.SessionId, true))">Log out</LinkButton>
</p>
</td>
</tr>
@@ -70,16 +80,16 @@ Small collection of tools to do not-so-everyday things.
<td>
<p>
@{
- string[] parts = session.UserId.Split(':');
+ string[] parts = session.Auth.UserId.Split(':');
}
<span>@parts[0][1..]</span> on <span>@parts[1]</span>
- @if (!string.IsNullOrWhiteSpace(session.Proxy)) {
- <span class="badge badge-info"> (proxied via @session.Proxy)</span>
+ @if (!string.IsNullOrWhiteSpace(session.Auth.Proxy)) {
+ <span class="badge badge-info"> (proxied via @session.Auth.Proxy)</span>
}
</p>
</td>
<td>
- <LinkButton OnClick="@(() => RemoveUser(session))">Remove</LinkButton>
+ <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
</td>
</tr>
}
@@ -99,19 +109,19 @@ Small collection of tools to do not-so-everyday things.
<td>
<p>
@{
- string[] parts = session.UserId.Split(':');
+ string[] parts = session.Auth.UserId.Split(':');
}
<span>@parts[0][1..]</span> on <span>@parts[1]</span>
- @if (!string.IsNullOrWhiteSpace(session.Proxy)) {
- <span class="badge badge-info"> (proxied via @session.Proxy)</span>
+ @if (!string.IsNullOrWhiteSpace(session.Auth.Proxy)) {
+ <span class="badge badge-info"> (proxied via @session.Auth.Proxy)</span>
}
</p>
</td>
<td>
- <LinkButton OnClick="@(() => Task.Run(()=>NavigationManager.NavigateTo($"/InvalidSession?ctx={session.AccessToken}")))">Re-login</LinkButton>
+ <LinkButton OnClick="@(() => Task.Run(() => NavigationManager.NavigateTo($"/InvalidSession?ctx={session.SessionId}")))">Re-login</LinkButton>
</td>
<td>
- <LinkButton OnClick="@(() => RemoveUser(session))">Remove</LinkButton>
+ <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
</td>
</tr>
}
@@ -127,67 +137,76 @@ Small collection of tools to do not-so-everyday things.
private const bool _debug = false;
#endif
- private class AuthInfo {
- public UserAuth? UserAuth { get; set; }
+ private class HomepageSessionInfo : RmuSessionStore.SessionInfo {
public UserInfo? UserInfo { get; set; }
public ServerVersionResponse? ServerVersion { get; set; }
public AuthenticatedHomeserverGeneric? Homeserver { get; set; }
}
- private readonly List<AuthInfo> _sessions = [];
- private readonly List<UserAuth> _offlineSessions = [];
- private readonly List<UserAuth> _invalidSessions = [];
- private LoginResponse? _currentSession;
- int scannedSessions = 0, totalSessions = 1;
+ private readonly List<HomepageSessionInfo> _sessions = [];
+ private readonly List<RmuSessionStore.SessionInfo> _offlineSessions = [];
+ private readonly List<RmuSessionStore.SessionInfo> _invalidSessions = [];
+ private RmuSessionStore.SessionInfo? _currentSession;
+ int scannedSessions, totalSessions = 1;
private SvgIdenticonGenerator _identiconGenerator = new();
protected override async Task OnInitializedAsync() {
Console.WriteLine("Index.OnInitializedAsync");
logger.LogDebug("Initialising index page");
- _currentSession = await RMUStorage.GetCurrentToken();
+
+ _currentSession = await sessionStore.GetCurrentSession();
_sessions.Clear();
_offlineSessions.Clear();
- var tokens = await RMUStorage.GetAllTokens();
+ var sessions = await sessionStore.GetAllSessions();
scannedSessions = 0;
- totalSessions = tokens.Count;
+ totalSessions = sessions.Count;
logger.LogDebug("Found {0} tokens", totalSessions);
- if (tokens is not { Count: > 0 }) {
+ if (sessions is not { Count: > 0 }) {
Console.WriteLine("No tokens found, trying migration from MRU...");
- await RMUStorage.MigrateFromMRU();
- tokens = await RMUStorage.GetAllTokens();
- if (tokens is not { Count: > 0 }) {
+ sessions = await sessionStore.GetAllSessions();
+ if (sessions is not { Count: > 0 }) {
Console.WriteLine("No tokens found");
return;
}
}
List<string> offlineServers = [];
- var sema = new SemaphoreSlim(64, 64);
+ var sema = new SemaphoreSlim(8, 8);
var updateSw = Stopwatch.StartNew();
- var tasks = tokens.Select(async token => {
+ var tasks = sessions.Select(async session => {
await sema.WaitAsync();
- if ((!string.IsNullOrWhiteSpace(token.Proxy) && offlineServers.Contains(token.Proxy)) || offlineServers.Contains(token.Homeserver)) {
- _offlineSessions.Add(token);
- sema.Release();
- scannedSessions++;
- return;
- }
+ var token = session.Value.Auth;
AuthenticatedHomeserverGeneric hs;
try {
- hs = await hsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy);
+ Task<ServerVersionResponse> serverVersionTask = Task.FromResult<ServerVersionResponse>(new() {
+ Server = new() {
+ Name = "Unknown",
+ Version = "0.0.0"
+ }
+ });
+ try {
+ hs = await HsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy);
+ serverVersionTask = hs.FederationClient?.GetServerVersionAsync() ?? serverVersionTask!;
+ }
+ catch (Exception e) {
+ logger.LogError("Failed to get info for {0} via {1}: {2}", token.UserId, token.Homeserver, e);
+ logger.LogError("Continuing with server-less session");
+ hs = await HsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy, useGeneric: true, enableServer: false);
+ }
+
var joinedRoomsTask = hs.GetJoinedRooms();
var profileTask = hs.GetProfileAsync(hs.WhoAmI.UserId);
- var serverVersionTask = hs.FederationClient?.GetServerVersionAsync();
_sessions.Add(new() {
+ Auth = token,
+ SessionId = session.Value.SessionId,
+ Homeserver = hs,
UserInfo = new() {
- AvatarUrl = string.IsNullOrWhiteSpace((await profileTask).AvatarUrl) ? _identiconGenerator.GenerateAsDataUri(hs.WhoAmI.UserId) : hs.ResolveMediaUri((await profileTask).AvatarUrl),
+ AvatarUrl = (await profileTask).AvatarUrl,
RoomCount = (await joinedRoomsTask).Count,
DisplayName = (await profileTask).DisplayName ?? hs.WhoAmI.UserId
},
- UserAuth = token,
ServerVersion = await (serverVersionTask ?? Task.FromResult<ServerVersionResponse?>(null)!),
- Homeserver = hs
});
if (updateSw.ElapsedMilliseconds > 25) {
updateSw.Restart();
@@ -197,7 +216,7 @@ Small collection of tools to do not-so-everyday things.
catch (MatrixException e) {
if (e is { ErrorCode: "M_UNKNOWN_TOKEN" }) {
logger.LogWarning("Got unknown token error for {0} via {1}", token.UserId, token.Homeserver);
- _invalidSessions.Add(token);
+ _invalidSessions.Add(session.Value);
}
else {
logger.LogError("Failed to get info for {0} via {1}: {2}", token.UserId, token.Homeserver, e);
@@ -226,15 +245,16 @@ Small collection of tools to do not-so-everyday things.
}
private class UserInfo {
- internal string AvatarUrl { get; set; }
+ internal string? AvatarUrl { get; set; }
internal string DisplayName { get; set; }
internal int RoomCount { get; set; }
}
- private async Task RemoveUser(UserAuth auth, bool logout = false) {
+ private async Task RemoveUser(string sessionId, bool logout = false) {
try {
if (logout) {
- await (await hsProvider.GetAuthenticatedWithToken(auth.Homeserver, auth.AccessToken, auth.Proxy)).Logout();
+ var auth = (await sessionStore.GetSession(sessionId))?.Auth;
+ await (await HsProvider.GetAuthenticatedWithToken(auth.Homeserver, auth.AccessToken, auth.Proxy)).Logout();
}
}
catch (Exception e) {
@@ -246,21 +266,19 @@ Small collection of tools to do not-so-everyday things.
Console.WriteLine(e);
}
- await RMUStorage.RemoveToken(auth);
- if ((await RMUStorage.GetCurrentToken())?.AccessToken == auth.AccessToken)
- await RMUStorage.SetCurrentToken((await RMUStorage.GetAllTokens() ?? throw new InvalidOperationException()).FirstOrDefault());
+ await sessionStore.RemoveSession(sessionId);
StateHasChanged();
}
- private async Task SwitchSession(UserAuth auth) {
- Console.WriteLine($"Switching to {auth.Homeserver} {auth.UserId} via {auth.Proxy}");
- await RMUStorage.SetCurrentToken(auth);
- _currentSession = auth;
+ private async Task SwitchSession(string sessionId) {
+ Console.WriteLine($"Switching to {sessionId}");
+ await sessionStore.SetCurrentSession(sessionId);
+ _currentSession = await sessionStore.GetCurrentSession();
StateHasChanged();
}
- private async Task ManageUser(UserAuth auth) {
- await SwitchSession(auth);
+ private async Task ManageUser(string sessionId) {
+ await sessionStore.SetCurrentSession(sessionId);
NavigationManager.NavigateTo("/User/Profile");
}
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/InvalidSession.razor b/MatrixUtils.Web/Pages/InvalidSession.razor
index e1a72ea..1ec99f6 100644
--- a/MatrixUtils.Web/Pages/InvalidSession.razor
+++ b/MatrixUtils.Web/Pages/InvalidSession.razor
@@ -6,14 +6,15 @@
<h3>Rory&::MatrixUtils - Invalid session encountered</h3>
<p>A session was encountered that is no longer valid. This can happen if you have logged out of the account on another device, or if the access token has expired.</p>
-@if (_login is not null) {
- <p>It appears that the affected user is @_login.UserId (@_login.DeviceId) on @_login.Homeserver!</p>
+@if (_auth is not null) {
+ <p>It appears that the affected user is @_auth.UserId (@_auth.DeviceId) on @_auth.Homeserver!</p>
<LinkButton OnClick="@(OpenRefreshDialog)">Refresh token</LinkButton>
<LinkButton OnClick="@(RemoveUser)">Remove</LinkButton>
@if (_showRefreshDialog) {
- <ModalWindow MinWidth="300" X="275" Y="300" Title="@($"Password for {_login.UserId}")">
- <FancyTextBox IsPassword="true" @bind-Value="@_password"></FancyTextBox><br/>
+ <ModalWindow MinWidth="300" X="275" Y="300" Title="@($"Password for {_auth.UserId}")">
+ <FancyTextBox IsPassword="true" @bind-Value="@_password"></FancyTextBox>
+ <br/>
<LinkButton OnClick="TryLogin">Log in</LinkButton>
@if (_loginException is not null) {
<pre style="color: red;">@_loginException.RawContent</pre>
@@ -29,9 +30,9 @@ else {
{
[Parameter]
[SupplyParameterFromQuery(Name = "ctx")]
- public string Context { get; set; }
+ public string SessionId { get; set; }
- private UserAuth? _login { get; set; }
+ private UserAuth? _auth { get; set; }
private bool _showRefreshDialog { get; set; }
@@ -40,25 +41,21 @@ else {
private MatrixException? _loginException { get; set; }
protected override async Task OnInitializedAsync() {
- var tokens = await RMUStorage.GetAllTokens();
- if (tokens is null || tokens.Count == 0) {
+ var tokens = await sessionStore.GetAllSessions();
+ if (tokens.Count == 0) {
NavigationManager.NavigateTo("/Login");
return;
}
- _login = tokens.FirstOrDefault(x => x.AccessToken == Context);
-
- if (_login is null) {
- Console.WriteLine($"Could not find {_login} in stored tokens!");
- }
+ if (tokens.TryGetValue(SessionId, out var session))
+ _auth = session.Auth;
+ else Console.WriteLine($"Could not find {SessionId} in stored sessions!");
await base.OnInitializedAsync();
}
private async Task RemoveUser() {
- await RMUStorage.RemoveToken(_login!);
- if ((await RMUStorage.GetCurrentToken())!.AccessToken == _login!.AccessToken)
- await RMUStorage.SetCurrentToken((await RMUStorage.GetAllTokens())?.FirstOrDefault());
+ await sessionStore.RemoveSession(SessionId);
await OnInitializedAsync();
}
@@ -68,30 +65,29 @@ else {
await Task.CompletedTask;
}
- private async Task SwitchSession(UserAuth auth) {
- Console.WriteLine($"Switching to {auth.Homeserver} {auth.AccessToken} {auth.UserId}");
- await RMUStorage.SetCurrentToken(auth);
+ private async Task SwitchSession(string sessionId) {
+ Console.WriteLine($"Switching to session {sessionId}");
+ await sessionStore.SetCurrentSession(sessionId);
await OnInitializedAsync();
}
private async Task TryLogin() {
- if(_login is null) throw new NullReferenceException("Login is null!");
+ if (_auth is null) throw new NullReferenceException("Login is null!");
try {
- var result = new UserAuth(await hsProvider.Login(_login.Homeserver, _login.UserId, _password));
+ var result = new UserAuth(await HsProvider.Login(_auth.Homeserver, _auth.UserId, _password));
if (result is null) {
- Console.WriteLine($"Failed to login to {_login.Homeserver} as {_login.UserId}!");
+ Console.WriteLine($"Failed to login to {_auth.Homeserver} as {_auth.UserId}!");
return;
}
+
Console.WriteLine($"Obtained access token for {result.UserId}!");
- await RemoveUser();
- await RMUStorage.AddToken(result);
- if (result.UserId == (await RMUStorage.GetCurrentToken())?.UserId)
- await RMUStorage.SetCurrentToken(result);
+ await sessionStore.RemoveSession(SessionId);
+ await sessionStore.AddSession(result);
NavigationManager.NavigateTo("/");
}
catch (MatrixException e) {
- Console.WriteLine($"Failed to login to {_login.Homeserver} as {_login.UserId}!");
+ Console.WriteLine($"Failed to login to {_auth.Homeserver} as {_auth.UserId}!");
Console.WriteLine(e);
_loginException = e;
StateHasChanged();
diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
index b370080..8831dd1 100644
--- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
+++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
@@ -10,6 +10,6 @@
@code {
[Parameter]
- public ClientContext Data { get; set; } = null!;
+ public ClientContext Data { get; set; }
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor
index c680c13..60f850d 100644
--- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor
+++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor
@@ -10,7 +10,7 @@
@code {
[Parameter]
- public ObservableCollection<ClientContext> Data { get; set; } = null!;
+ public ObservableCollection<ClientContext> Data { get; set; }
protected override void OnInitialized() {
Data.CollectionChanged += (_, e) => {
diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor
index 67dcae5..6a930b1 100644
--- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor
+++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor
@@ -25,6 +25,6 @@
@code {
[Parameter]
- public Index.ClientContext Data { get; set; } = null!;
+ public Index.ClientContext Data { get; set; }
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Labs/Client/Index.razor b/MatrixUtils.Web/Pages/Labs/Client/Index.razor
index ef4a0b9..c6e7d1a 100644
--- a/MatrixUtils.Web/Pages/Labs/Client/Index.razor
+++ b/MatrixUtils.Web/Pages/Labs/Client/Index.razor
@@ -40,11 +40,11 @@
}
protected override async Task OnInitializedAsync() {
- var tokens = await RMUStorage.GetAllTokens();
- var tasks = tokens.Select(async token => {
+ var tokens = await sessionStore.GetAllSessions();
+ var tasks = tokens.Keys.Select(async token => {
try {
var cc = new ClientContext() {
- Homeserver = await RMUStorage.GetSession(token)
+ Homeserver = await sessionStore.GetHomeserver(token)
};
cc.SyncWrapper = new ClientSyncWrapper(cc.Homeserver);
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor
index c0dc8a6..f81afe5 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor
@@ -52,7 +52,7 @@
NavigationManager.NavigateTo(NavigationManager.Uri.Replace("stage=", ""), true); //"/User/DMSpace/Setup"
}
DMSpaceRootPage = this;
- SetupData.Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate();
+ SetupData.Homeserver ??= await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (SetupData.Homeserver is null) return;
try {
SetupData.DmSpaceConfiguration = await SetupData.Homeserver.GetAccountDataAsync<DMSpaceConfiguration>("gay.rory.dm_space");
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
index 55e17d6..a974a8f 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
@@ -4,7 +4,8 @@
@using MatrixUtils.LibDMSpace
@using MatrixUtils.LibDMSpace.StateEvents
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.EventTypes.Spec.State.Space
@using MatrixUtils.Abstractions
<b>
<u>DM Space setup tool - stage 1: Configure space</u>
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
index be6027a..b8eb257 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
@@ -1,6 +1,6 @@
@using LibMatrix.RoomTypes
-@using LibMatrix.EventTypes.Spec.State
@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using MatrixUtils.Abstractions
<b>
<u>DM Space setup tool - stage 2: Fix DM room attribution</u>
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
index 09de5d3..dac9c49 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
@@ -1,8 +1,8 @@
@using LibMatrix.RoomTypes
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.Responses
@using MatrixUtils.LibDMSpace
@using System.Text.Json.Serialization
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using MatrixUtils.Abstractions
<b>
diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor
index 3392960..441752b 100644
--- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor
+++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor
@@ -55,7 +55,7 @@
public RoomListViewData Data { get; set; } = new RoomListViewData();
protected override async Task OnInitializedAsync() {
- Data.Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Data.Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Data.Homeserver is null) return;
var rooms = await Data.Homeserver.GetJoinedRooms();
Data.GlobalProfile = await Data.Homeserver.GetProfileAsync(Data.Homeserver.WhoAmI.UserId);
diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor
index 6483f01..ba994d1 100644
--- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor
+++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor
@@ -1,14 +1,14 @@
@using MatrixUtils.Abstractions
-<div class="spaceListItem" style="@(SelectedSpace == Space ? "background-color: #FFFFFF33;" : "")" onclick="@SelectSpace">
+<div class="spaceListItem" style="@(SelectedSpace == Space ? "background-color: #FFFFFF33;" : "")" @onclick="@SelectSpace">
<div class="spaceListItemContainer">
@if (IsSpaceOpened()) {
- <span onclick="@ToggleSpace">▼ </span>
+ <span @onclick="@ToggleSpace">▼ </span>
}
else {
- <span onclick="@ToggleSpace">▶ </span>
+ <span @onclick="@ToggleSpace">▶ </span>
}
- <MxcImage Circular="true" Height="32" Width="32" Homeserver="Space.Room.Homeserver" MxcUri="@Space.RoomIcon"></MxcImage>
+ <MxcImage Homeserver="@Homeserver" Circular="true" Height="32" Width="32" Uri="@Space.RoomIcon"></MxcImage>
<span class="spaceNameEllipsis">@Space.RoomName</span>
</div>
@if (IsSpaceOpened()) {
@@ -30,6 +30,9 @@
[Parameter]
public List<RoomInfo> OpenedSpaces { get; set; }
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
protected override Task OnInitializedAsync() {
Space.PropertyChanged += (sender, args) => { StateHasChanged(); };
return base.OnInitializedAsync();
diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
index f4cf849..79f931b 100644
--- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
+++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
@@ -22,7 +22,7 @@
@code {
[CascadingParameter]
- public Index2.RoomListViewData Data { get; set; } = null!;
+ public Index2.RoomListViewData Data { get; set; }
protected override async Task OnInitializedAsync() {
Data.Rooms.CollectionChanged += (sender, args) => {
diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor
index f4cf849..79f931b 100644
--- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor
+++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor
@@ -22,7 +22,7 @@
@code {
[CascadingParameter]
- public Index2.RoomListViewData Data { get; set; } = null!;
+ public Index2.RoomListViewData Data { get; set; }
protected override async Task OnInitializedAsync() {
Data.Rooms.CollectionChanged += (sender, args) => {
diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor
index 7ccfae2..99b031a 100644
--- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor
+++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor
@@ -1,6 +1,6 @@
@using MatrixUtils.Abstractions
@using System.ComponentModel
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.Space
@using MatrixUtils.Web.Pages.Labs.Rooms2.Index2Components.MainTabComponents
<h3>RoomsIndex2MainTab</h3>
@@ -22,31 +22,32 @@
@* </div> *@
@* </div> *@
-<div>
- <div class="row">
- <div class="col-3" style="background-color: #ffffff22;">
- <LinkButton>Uncategorised rooms</LinkButton>
- @foreach (var space in GetTopLevelSpaces()) {
- @* @RecursingSpaceChildren(space) *@
- <MainTabSpaceItem Space="space" OpenedSpaces="OpenedSpaces" @bind-SelectedSpace="SelectedSpace" />
- }
- </div>
- <div class="col-9" style="background-color: #ff00ff66;">
- <p>Placeholder for rooms list...</p>
- @if (SelectedSpace != null) {
- foreach (var room in GetSpaceChildRooms(SelectedSpace)) {
- <p>@room.RoomName</p>
+<CascadingValue Name="Homeserver" Value="@Data.Homeserver">
+ <div>
+ <div class="row">
+ <div class="col-3" style="background-color: #ffffff22;">
+ <LinkButton>Uncategorised rooms</LinkButton>
+ @foreach (var space in GetTopLevelSpaces()) {
+ @* @RecursingSpaceChildren(space) *@
+ <MainTabSpaceItem Space="space" OpenedSpaces="OpenedSpaces" @bind-SelectedSpace="SelectedSpace"/>
+ }
+ </div>
+ <div class="col-9" style="background-color: #ff00ff66;">
+ <p>Placeholder for rooms list...</p>
+ @if (SelectedSpace != null) {
+ foreach (var room in GetSpaceChildRooms(SelectedSpace)) {
+ <p>@room.RoomName</p>
+ }
}
- }
+ </div>
</div>
</div>
-</div>
-
+</CascadingValue>
@code {
[CascadingParameter]
- public Index2.RoomListViewData Data { get; set; } = null!;
+ public Index2.RoomListViewData Data { get; set; }
protected override async Task OnInitializedAsync() {
Data.Rooms.CollectionChanged += (sender, args) => {
@@ -118,7 +119,7 @@
var childSpaces = children.Where(x => x.RoomType == "m.space").ToList();
return childSpaces;
}
-
+
private List<RoomInfo> GetSpaceChildRooms(RoomInfo space) {
var children = GetSpaceChildren(space);
var childRooms = children.Where(x => x.RoomType != "m.space").ToList();
diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor
index 91f228d..33c310a 100644
--- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor
+++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor
@@ -2,11 +2,11 @@
@using LibMatrix.Responses
@using MatrixUtils.Abstractions
@using System.Diagnostics
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.Extensions
@using LibMatrix.Utilities
@using System.Collections.ObjectModel
@using ArcaneLibs
+@using LibMatrix.EventTypes.Spec.State.Space
@inject ILogger<RoomsIndex2SyncContainer> logger
<pre>RoomsIndex2SyncContainer</pre>
@foreach (var (name, value) in _statusList) {
@@ -16,7 +16,7 @@
@code {
[Parameter]
- public Index2.RoomListViewData Data { get; set; } = null!;
+ public Index2.RoomListViewData Data { get; set; }
private SyncHelper syncHelper;
@@ -113,7 +113,7 @@
statusd.Status = $"{roomId} already known with {room.StateEvents?.Count ?? 0} state events";
}
else {
- statusd.Status = $"Eencountered new room {roomId}!";
+ statusd.Status = $"Encountered new room {roomId}!";
room = new RoomInfo(Data.Homeserver!.GetRoom(roomId), roomData.State?.Events);
Data.Rooms.Add(room);
}
diff --git a/MatrixUtils.Web/Pages/LoginPage.razor b/MatrixUtils.Web/Pages/LoginPage.razor
index 6c869ac..88577a2 100644
--- a/MatrixUtils.Web/Pages/LoginPage.razor
+++ b/MatrixUtils.Web/Pages/LoginPage.razor
@@ -27,6 +27,27 @@
<br/>
<br/>
+
+<h4>Add with access token</h4>
+<hr/>
+
+<span style="display: block;">
+ <label>Homeserver:</label>
+ <FancyTextBox @bind-Value="@newRecordInput.Homeserver"></FancyTextBox>
+</span>
+<span style="display: block;">
+ <label>Access token:</label>
+ <FancyTextBox @bind-Value="@newRecordInput.Password" IsPassword="true"></FancyTextBox>
+</span>
+<span style="display: block">
+ <label>Proxy (<a href="https://cgit.rory.gay/matrix/MxApiExtensions.git">MxApiExtensions</a> or similar):</label>
+ <FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox>
+</span>
+<br/>
+<LinkButton OnClick="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton>
+<br/>
+<br/>
+
<h4>Import from TSV</h4>
<hr/>
<span>Import credentials from a TSV (Tab Separated Values) file</span><br/>
@@ -47,7 +68,7 @@
</thead>
@foreach (var record in records) {
var r = record;
- <tr style="background-color: @(LoggedInSessions.Any(x => x.UserId == $"@{r.Username}:{r.Homeserver}" && x.Proxy == r.Proxy) ? "green" : "unset")">
+ <tr style="background-color: @(LoggedInSessions.Any(x => x.Value.Auth.UserId == $"@{r.Username}:{r.Homeserver}" && x.Value.Auth.Proxy == r.Proxy) ? "green" : "unset")">
<td style="border-width: 1px;">
<FancyTextBox @bind-Value="@r.Username"></FancyTextBox>
</td>
@@ -87,7 +108,7 @@
readonly List<LoginStruct> records = new();
private LoginStruct newRecordInput = new();
- List<UserAuth>? LoggedInSessions { get; set; } = new();
+ Dictionary<string, RmuSessionStore.SessionInfo> LoggedInSessions { get; set; } = new();
async Task LoginAll() {
var loginTasks = records.Select(Login);
@@ -97,10 +118,10 @@
async Task Login(LoginStruct record) {
if (!records.Contains(record))
records.Add(record);
- if (LoggedInSessions.Any(x => x.UserId == $"@{record.Username}:{record.Homeserver}" && x.Proxy == record.Proxy)) return;
+ if (LoggedInSessions.Any(x => x.Value.Auth.UserId == $"@{record.Username}:{record.Homeserver}" && x.Value.Auth.UserId == record.Proxy)) return;
StateHasChanged();
try {
- var result = new UserAuth(await hsProvider.Login(record.Homeserver, record.Username, record.Password, record.Proxy)) {
+ var result = new UserAuth(await HsProvider.Login(record.Homeserver, record.Username, record.Password, record.Proxy)) {
Proxy = record.Proxy
};
if (result == null) {
@@ -110,8 +131,8 @@
Console.WriteLine($"Obtained access token for {result.UserId}!");
- await RMUStorage.AddToken(result);
- LoggedInSessions = await RMUStorage.GetAllTokens();
+ await sessionStore.AddSession(result);
+ LoggedInSessions = await sessionStore.GetAllSessions();
}
catch (Exception e) {
Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!");
@@ -123,7 +144,7 @@
}
private async Task FileChanged(InputFileChangeEventArgs obj) {
- LoggedInSessions = await RMUStorage.GetAllTokens();
+ LoggedInSessions = await sessionStore.GetAllSessions();
Console.WriteLine(JsonSerializer.Serialize(obj, new JsonSerializerOptions {
WriteIndented = true
}));
@@ -141,7 +162,7 @@
}
private async Task AddRecord() {
- LoggedInSessions = await RMUStorage.GetAllTokens();
+ LoggedInSessions = await sessionStore.GetAllSessions();
records.Add(newRecordInput);
newRecordInput = new();
}
@@ -156,4 +177,27 @@
internal Exception? Exception { get; set; }
}
+ private async Task AddWithAccessToken(LoginStruct record) {
+ try {
+ var session = await HsProvider.GetAuthenticatedWithToken(record.Homeserver, record.Password, record.Proxy);
+ if (session == null) {
+ Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!");
+ return;
+ }
+
+ await sessionStore.AddSession(new UserAuth() {
+ UserId = session.WhoAmI.UserId,
+ AccessToken = session.AccessToken,
+ Proxy = record.Proxy,
+ DeviceId = session.WhoAmI.DeviceId
+ });
+ LoggedInSessions = await sessionStore.GetAllSessions();
+ }
+ catch (Exception e) {
+ Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!");
+ Console.WriteLine(e);
+ record.Exception = e;
+ }
+ }
+
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
index 9218c8c..e30adf6 100644
--- a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
+++ b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
@@ -1,7 +1,7 @@
@page "/Moderation/UserRoomHistory/{UserId}"
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.RoomTypes
@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using MatrixUtils.Abstractions
<h3>UserRoomHistory</h3>
@@ -44,11 +44,11 @@ else {
private AuthenticatedHomeserverGeneric? currentHs { get; set; }
protected override async Task OnInitializedAsync() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
- var sessions = await RMUStorage.GetAllTokens();
- foreach (var userAuth in sessions) {
- var session = await RMUStorage.GetSession(userAuth);
+ var sessions = await sessionStore.GetAllSessions();
+ foreach (var userAuth in sessions.Keys) {
+ var session = await sessionStore.GetHomeserver(userAuth);
if (session is not null) {
hss.Add(session);
StateHasChanged();
diff --git a/MatrixUtils.Web/Pages/Rooms/Create.razor b/MatrixUtils.Web/Pages/Rooms/Create.razor
index f2dfb01..021ad18 100644
--- a/MatrixUtils.Web/Pages/Rooms/Create.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Create.razor
@@ -3,11 +3,9 @@
@using System.Reflection
@using ArcaneLibs.Extensions
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.Responses
@using MatrixUtils.Web.Classes.RoomCreationTemplates
-@using Microsoft.AspNetCore.Components.Forms
@* @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not */ *@
<h3>Room Manager - Create Room</h3>
@@ -89,7 +87,7 @@
<tr>
<td>Room icon:</td>
<td>
- <img src="@Homeserver.ResolveMediaUri(roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/>
+ @* <img src="@Homeserver.ResolveMediaUri(roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/> *@
<div style="display: inline-block; vertical-align: middle;">
<FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox><br/>
<InputFile OnChange="RoomIconFilePicked"></InputFile>
@@ -134,7 +132,7 @@
}
else {
<details>
- <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerACLEventContent).Allow.Count) allow rules</summary>
+ <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerAclEventContent).Allow.Count) allow rules</summary>
@* <StringListEditor @bind-Items="@serverAcl.Allow"></StringListEditor> *@
</details>
}
@@ -144,7 +142,7 @@
}
else {
<details>
- <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerACLEventContent).Deny.Count) deny rules</summary>
+ <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerAclEventContent).Deny.Count) deny rules</summary>
@* <StringListEditor @bind-Items="@serverAcl.Allow"></StringListEditor> *@
</details>
}
@@ -256,11 +254,11 @@
private RoomHistoryVisibilityEventContent? historyVisibility => creationEvent?["m.room.history_visibility"].TypedContent as RoomHistoryVisibilityEventContent;
private RoomGuestAccessEventContent? guestAccessEvent => creationEvent?["m.room.guest_access"].TypedContent as RoomGuestAccessEventContent;
- private RoomServerACLEventContent? serverAcl => creationEvent?["m.room.server_acls"].TypedContent as RoomServerACLEventContent;
+ private RoomServerAclEventContent? serverAcl => creationEvent?["m.room.server_acls"].TypedContent as RoomServerAclEventContent;
private RoomAvatarEventContent? roomAvatarEvent => creationEvent?["m.room.avatar"].TypedContent as RoomAvatarEventContent;
protected override async Task OnInitializedAsync() {
- Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) {
diff --git a/MatrixUtils.Web/Pages/Rooms/Index.razor b/MatrixUtils.Web/Pages/Rooms/Index.razor
index 28c4de2..0373a46 100644
--- a/MatrixUtils.Web/Pages/Rooms/Index.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Index.razor
@@ -13,9 +13,7 @@
<p>@Status2</p>
<LinkButton href="/Rooms/Create">Create new room</LinkButton>
-<CascadingValue TValue="AuthenticatedHomeserverGeneric" Value="Homeserver">
- <RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents"></RoomList>
-</CascadingValue>
+<RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents" Homeserver="@Homeserver"></RoomList>
@code {
@@ -68,7 +66,7 @@
// SyncHelper profileSyncHelper;
protected override async Task OnInitializedAsync() {
- Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
// var rooms = await Homeserver.GetJoinedRooms();
// SemaphoreSlim _semaphore = new(160, 160);
@@ -122,7 +120,7 @@
try {
while (queue.Count == 0) {
Console.WriteLine("Queue is empty, waiting...");
- await Task.Delay(isInitialSync ? 100 : 2500);
+ await Task.Delay(isInitialSync ? 1000 : 2500);
}
Console.WriteLine($"Queue no longer empty after {renderTimeSw.Elapsed}!");
@@ -131,15 +129,15 @@
isInitialSync = false;
while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) {
var (roomId, roomData) = queueEntry;
- Console.WriteLine($"Dequeued room {roomId}");
+ // Console.WriteLine($"Dequeued room {roomId}");
RoomInfo room;
if (Rooms.Any(x => x.Room.RoomId == roomId)) {
room = Rooms.First(x => x.Room.RoomId == roomId);
- Console.WriteLine($"QueueWorker: {roomId} already known with {room.StateEvents?.Count ?? 0} state events");
+ // Console.WriteLine($"QueueWorker: {roomId} already known with {room.StateEvents?.Count ?? 0} state events");
}
else {
- Console.WriteLine($"QueueWorker: encountered new room {roomId}!");
+ // Console.WriteLine($"QueueWorker: encountered new room {roomId}!");
room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.State?.Events);
Rooms.Add(room);
}
@@ -155,6 +153,11 @@
Console.WriteLine($"QueueWorker: could not merge state for {room.Room.RoomId} as new data contains no state events!");
}
+ if (maxUpdates % 100 == 0) {
+ Console.WriteLine($"QueueWorker: {queue.Count} entries left in queue, {maxUpdates} maxUpdates left, RenderContents: {RenderContents}");
+ StateHasChanged();
+ await Task.Yield();
+ }
// await Task.Delay(100);
}
@@ -170,7 +173,7 @@
}
}
- private bool RenderContents { get; set; } = false;
+ private bool RenderContents { get; set; }
private string _status;
@@ -225,9 +228,10 @@
Rooms.Remove(Rooms.First(x => x.Room.RoomId == leftRoom.Key));
Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue... " +
- $"{sync?.Rooms?.Join?.Count ?? 0} new updates!";
+ $"{sync.Rooms?.Join?.Count ?? 0} new updates!";
- Status2 = $"Next batch: {sync.NextBatch}";
+ Status2 = $"Next batch: {sync?.NextBatch}";
+ await Task.Yield();
}
}
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index b7ebae2..9c35673 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -1,7 +1,6 @@
@page "/Rooms/{RoomId}/Policies"
@using LibMatrix
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.EventTypes.Spec.State.Policy
@using System.Diagnostics
@using LibMatrix.RoomTypes
@@ -9,13 +8,25 @@
@using System.Reflection
@using ArcaneLibs.Attributes
@using LibMatrix.EventTypes
+@using LibMatrix.EventTypes.Common
+@using LibMatrix.EventTypes.Interop.Draupnir
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using MatrixUtils.Web.Shared.PolicyEditorComponents
+@using SpawnDev.BlazorJS.WebWorkers
+@inject WebWorkerService WebWorkerService
-<h3>Policy list editor - Editing @RoomId</h3>
+<h3>Policy list editor - Editing @(RoomName ?? RoomId)</h3>
+@if (!string.IsNullOrWhiteSpace(DraupnirShortcode)) {
+ <span style="margin-right: 2em;">Shortcode: @DraupnirShortcode</span>
+}
+@if (!string.IsNullOrWhiteSpace(RoomAlias)) {
+ <span>Alias: @RoomAlias</span>
+}
<hr/>
@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@
<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton>
+<LinkButton OnClick="@(() => { MassCreatePolicies = true; return Task.CompletedTask; })">Create many new policies</LinkButton>
@if (Loading) {
<p>Loading...</p>
@@ -24,6 +35,8 @@ else if (PolicyEventsByType is not { Count: > 0 }) {
<p>No policies yet</p>
}
else {
+ var renderSw = Stopwatch.StartNew();
+ var renderTotalSw = Stopwatch.StartNew();
@foreach (var (type, value) in PolicyEventsByType) {
<p>
@(GetValidPolicyEventsByType(type).Count) active,
@@ -33,6 +46,8 @@ else {
</p>
}
+ Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}");
+
@foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) {
<details>
<summary>
@@ -41,7 +56,7 @@ else {
</span>
<hr style="margin: revert;"/>
</summary>
- <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
+ <table class="table table-striped table-hover">
@{
var policies = GetValidPolicyEventsByType(type);
var invalidPolicies = GetInvalidPolicyEventsByType(type);
@@ -51,13 +66,18 @@ else {
.Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
.ToFrozenSet();
var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet();
+
+ var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => props.Any(y => y.Name == x.Name))
+ .ToFrozenSet();
+ Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
}
<thead>
<tr>
@foreach (var name in propNames) {
- <th style="border-width: 1px">@name</th>
+ <th>@name</th>
}
- <th style="border-width: 1px">Actions</th>
+ <th>Actions</th>
</tr>
</thead>
<tbody style="border-width: 1px;">
@@ -65,10 +85,6 @@ else {
<tr>
@{
var typedContent = policy.TypedContent!;
- var proxySafeProps = typedContent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
- .Where(x => props.Any(y => y.Name == x.Name))
- .ToFrozenSet();
- Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
}
@foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) {
<td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td>
@@ -81,6 +97,16 @@ else {
@if (policy.IsLegacyType) {
<LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
}
+
+ @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.Type)) {
+ <LinkButton OnClick="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent</LinkButton>
+ @if (CurrentUserIsDraupnir) {
+ <LinkButton Color="@(ActiveKicks.ContainsKey(policy) ? "#FF0000" : null)" OnClick="@(() => DraupnirKickMatching(policy))">Kick users @(ActiveKicks.ContainsKey(policy) ? $"({ActiveKicks[policy]})" : null)</LinkButton>
+ }
+ }
+ }
+ else {
+ <p>No permission to modify</p>
}
</div>
</td>
@@ -94,11 +120,11 @@ else {
@("Invalid " + GetPolicyTypeName(type).ToLower())
</u>
</summary>
- <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
+ <table class="table table-striped table-hover">
<thead>
<tr>
- <th style="border-width: 1px">State key</th>
- <th style="border-width: 1px">Json contents</th>
+ <th>State key</th>
+ <th>Json contents</th>
</tr>
</thead>
<tbody>
@@ -115,12 +141,25 @@ else {
</details>
</details>
}
+
+ Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
+ Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}");
}
@if (CurrentlyEditingEvent is not null) {
<PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal>
}
+@if (ServerPolicyToMakePermanent is not null) {
+ <ModalWindow Title="Make policy permanent">
+
+ </ModalWindow>
+}
+
+@if (MassCreatePolicies) {
+ <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => { MassCreatePolicies = false; LoadStatesAsync(); })"></MassPolicyEditorModal>
+}
+
@code {
#if DEBUG
@@ -130,21 +169,15 @@ else {
#endif
private bool Loading { get; set; } = true;
- //get room list
- // - sync withroom list filter
- // Type = support.feline.msc3784
- //support.feline.policy.lists.msc.v1
[Parameter]
- public string RoomId { get; set; } = null!;
+ public string RoomId { get; set; }
private bool _enableAvatars;
private StateEventResponse? _currentlyEditingEvent;
+ private bool _massCreatePolicies;
+ private StateEventResponse? _serverPolicyToMakePermanent;
- // static readonly Dictionary<string, string?> Avatars = new();
- // static readonly Dictionary<string, RemoteHomeserver> Servers = new();
-
- // private static List<StateEventResponse> PolicyEvents { get; set; } = new();
private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
private StateEventResponse? CurrentlyEditingEvent {
@@ -155,25 +188,44 @@ else {
}
}
- // public bool EnableAvatars {
- // get => _enableAvatars;
- // set {
- // _enableAvatars = value;
- // if (value) GetAllAvatars();
- // }
- // }
+ public StateEventResponse? ServerPolicyToMakePermanent {
+ get => _serverPolicyToMakePermanent;
+ set {
+ _serverPolicyToMakePermanent = value;
+ StateHasChanged();
+ }
+ }
private AuthenticatedHomeserverGeneric Homeserver { get; set; }
private GenericRoom Room { get; set; }
private RoomPowerLevelEventContent PowerLevels { get; set; }
+ public bool CurrentUserIsDraupnir { get; set; }
+ public string? RoomName { get; set; }
+ public string? RoomAlias { get; set; }
+ public string? DraupnirShortcode { get; set; }
+ public Dictionary<StateEventResponse, int> ActiveKicks { get; set; } = [];
+
+ public bool MassCreatePolicies {
+ get => _massCreatePolicies;
+ set {
+ _massCreatePolicies = value;
+ StateHasChanged();
+ }
+ }
protected override async Task OnInitializedAsync() {
var sw = Stopwatch.StartNew();
await base.OnInitializedAsync();
- Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!;
+ Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!;
if (Homeserver is null) return;
Room = Homeserver.GetRoom(RoomId!);
- PowerLevels = (await Room.GetPowerLevelsAsync())!;
+ await Task.WhenAll([
+ Task.Run(async () => { PowerLevels = (await Room.GetPowerLevelsAsync())!; }),
+ Task.Run(async () => { DraupnirShortcode = (await Room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode; }),
+ Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }),
+ Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); }),
+ Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>("org.matrix.mjolnir.protected_rooms")) is not null; }),
+ ]);
await LoadStatesAsync();
Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
}
@@ -193,48 +245,13 @@ else {
StateHasChanged();
}
- // private async Task GetAllAvatars() {
- // // if (!_enableAvatars) return;
- // Console.WriteLine("Getting avatars...");
- // var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue<string>()).Where(x => x.Contains(':') && !x.Contains("*")).ToList();
- // Console.WriteLine($"Got {users.Count} users!");
- // var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList());
- // Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!");
- // var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable();
- // await foreach (var server in homeserverTasks) {
- // if (server is null) continue;
- // var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList();
- // await Task.WhenAll(profileTasks);
- // profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } });
- // foreach (var profile in profileTasks.Select(x => x.Result!.Value)) {
- // // if (profile is null) continue;
- // if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) {
- // var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl);
- // Avatars.TryAdd(profile.Key, url);
- // }
- // else Avatars.TryAdd(profile.Key, null);
- // }
- //
- // StateHasChanged();
- // }
- // }
- //
- // private async Task<KeyValuePair<string, UserProfileResponse>?> TryGetProfile(RemoteHomeserver server, string mxid) {
- // try {
- // return new KeyValuePair<string, UserProfileResponse>(mxid, await server.GetProfileAsync(mxid));
- // }
- // catch {
- // return null;
- // }
- // }
-
private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
- .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+ .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
- .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+ .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
?? type.GetCustomAttributes<MatrixEventAttribute>()
@@ -265,4 +282,139 @@ else {
private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
.ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+ private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes
+ .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray());
+
+#region Draupnir interop
+
+ private SemaphoreSlim ss = new(16, 16);
+
+ private async Task DraupnirKickMatching(StateEventResponse policy) {
+ try {
+ var content = policy.TypedContent! as PolicyRuleEventContent;
+ if (content is null) return;
+ if (string.IsNullOrWhiteSpace(content.Entity)) return;
+
+ var data = await Homeserver.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId);
+ var rooms = data.Rooms.Select(Homeserver.GetRoom).ToList();
+
+ ActiveKicks.Add(policy, rooms.Count);
+ StateHasChanged();
+ await Task.Delay(500);
+
+ // for (int i = 0; i < 12; i++) {
+ // _ = WebWorkerService.TaskPool.Invoke(WasteCpu);
+ // }
+
+ // static async Task runKicks(string roomId, PolicyRuleEventContent content) {
+ // Console.WriteLine($"Checking {roomId}...");
+ // // Console.WriteLine($"Checking {room.RoomId}...");
+ // //
+ // // try {
+ // // var members = await room.GetMembersListAsync();
+ // // foreach (var member in members) {
+ // // var membership = member.ContentAs<RoomMemberEventContent>();
+ // // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue;
+ // // if (membership?.Membership is "leave" or "ban") continue;
+ // //
+ // // if (content.EntityMatches(member.StateKey!))
+ // // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given");
+ // // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)");
+ // // }
+ // // }
+ // // finally {
+ // // Console.WriteLine($"Finished checking {room.RoomId}...");
+ // // }
+ // }
+ //
+ // try {
+ // var tasks = rooms.Select(room => WebWorkerService.TaskPool.Invoke(runKicks, room.RoomId, content)).ToList();
+ //
+ // await Task.WhenAll(tasks);
+ // }
+ // catch (Exception e) {
+ // Console.WriteLine(e);
+ // }
+
+ await NastyInternalsPleaseIgnore.ExecuteKickWithWasmWorkers(WebWorkerService, Homeserver, policy, data.Rooms);
+ // await Task.Run(async () => {
+ // foreach (var room in rooms) {
+ // try {
+ // Console.WriteLine($"Checking {room.RoomId}...");
+ // var members = await room.GetMembersListAsync();
+ // foreach (var member in members) {
+ // var membership = member.ContentAs<RoomMemberEventContent>();
+ // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue;
+ // if (membership?.Membership is "leave" or "ban") continue;
+ //
+ // if (content.EntityMatches(member.StateKey!))
+ // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given");
+ // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)");
+ // }
+ // ActiveKicks[policy]--;
+ // StateHasChanged();
+ // }
+ // finally {
+ // Console.WriteLine($"Finished checking {room.RoomId}...");
+ // }
+ // }
+ // });
+ }
+ finally {
+ ActiveKicks.Remove(policy);
+ StateHasChanged();
+ await Task.Delay(500);
+ }
+ }
+
+#region Nasty, nasty internals, please ignore!
+
+ private static class NastyInternalsPleaseIgnore {
+ public static async Task ExecuteKickWithWasmWorkers(WebWorkerService workerService, AuthenticatedHomeserverGeneric hs, StateEventResponse evt, List<string> roomIds) {
+ try {
+ // var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal, hs.WellKnownUris.Client, hs.AccessToken, roomId, content.Entity)).ToList();
+ var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal2, hs.WellKnownUris, hs.AccessToken, roomId, evt)).ToList();
+ // workerService.TaskPool.Invoke(ExecuteKickInternal, hs.BaseUrl, hs.AccessToken, roomIds, content.Entity);
+ await Task.WhenAll(tasks);
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ }
+ }
+
+ private static async Task ExecuteKickInternal(string homeserverBaseUrl, string accessToken, string roomId, string entity) {
+ try {
+ Console.WriteLine("args: " + string.Join(", ", homeserverBaseUrl, accessToken, roomId, entity));
+ Console.WriteLine($"Checking {roomId}...");
+ var hs = new AuthenticatedHomeserverGeneric(homeserverBaseUrl, new() { Client = homeserverBaseUrl }, null, accessToken);
+ Console.WriteLine($"Got HS...");
+ var room = hs.GetRoom(roomId);
+ Console.WriteLine($"Got room...");
+ var members = await room.GetMembersListAsync();
+ Console.WriteLine($"Got members...");
+ // foreach (var member in members) {
+ // var membership = member.ContentAs<RoomMemberEventContent>();
+ // if (member.StateKey == hs.WhoAmI.UserId) continue;
+ // if (membership?.Membership is "leave" or "ban") continue;
+ //
+ // if (entity == member.StateKey)
+ // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given");
+ // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)");
+ // }
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ }
+ }
+
+ private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, StateEventResponse policy) {
+ Console.WriteLine($"Checking {roomId}...");
+ Console.WriteLine(policy.EventId);
+ }
+ }
+
+#endregion
+
+#endregion
+
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css
new file mode 100644
index 0000000..afe9fb0
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css
@@ -0,0 +1,9 @@
+th {
+ border-width: 1px;
+}
+
+table {
+ width: fit-content;
+ border-width: 1px;
+ vertical-align: middle;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
new file mode 100644
index 0000000..982fc5a
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
@@ -0,0 +1,240 @@
+@page "/Rooms/{RoomId}/Policies2"
+@using LibMatrix
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using System.Diagnostics
+@using LibMatrix.RoomTypes
+@using System.Collections.Frozen
+@using System.Reflection
+@using ArcaneLibs.Attributes
+@using LibMatrix.EventTypes
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+
+@using MatrixUtils.Web.Shared.PolicyEditorComponents
+
+<h3>Policy list editor - Editing @RoomId</h3>
+<hr/>
+@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@
+<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton>
+
+@if (Loading) {
+ <p>Loading...</p>
+}
+else if (PolicyEventsByType is not { Count: > 0 }) {
+ <p>No policies yet</p>
+}
+else {
+ var renderSw = Stopwatch.StartNew();
+ var renderTotalSw = Stopwatch.StartNew();
+ @foreach (var (type, value) in PolicyEventsByType) {
+ <p>
+ @(GetValidPolicyEventsByType(type).Count) active,
+ @(GetInvalidPolicyEventsByType(type).Count) invalid
+ (@value.Count total)
+ @(GetPolicyTypeName(type).ToLower())
+ </p>
+ }
+
+ Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}");
+
+ @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) {
+ <details>
+ <summary>
+ <span>
+ @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies")
+ </span>
+ <hr style="margin: revert;"/>
+ </summary>
+ <div class="flex-grid">
+ @{
+ var policies = GetValidPolicyEventsByType(type);
+ var invalidPolicies = GetInvalidPolicyEventsByType(type);
+ // enumerate all properties with friendly name
+ var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null)
+ .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
+ .ToFrozenSet();
+ var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet();
+
+ var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => props.Any(y => y.Name == x.Name))
+ .ToFrozenSet();
+ Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
+ }
+ @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) {
+ <div class="flex-item">
+ @{
+ var typedContent = policy.TypedContent!;
+ }
+ @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) {
+ <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td>
+ }
+ <div style="display: ruby;">
+ @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) {
+ <LinkButton OnClick="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton>
+ <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Remove</LinkButton>
+ @if (policy.IsLegacyType) {
+ <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
+ }
+
+ @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.EventId)) {
+ <LinkButton OnClick="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent (wildcard)</LinkButton>
+ @if (CurrentUserIsDraupnir) {
+ <LinkButton OnClick="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton>
+ }
+ }
+ else {
+ <p>meow</p>
+ }
+ }
+ else {
+ <p>No permission to modify</p>
+ }
+ </div>
+ </div>
+ }
+ </div>
+ <details>
+ <summary>
+ <u>
+ @("Invalid " + GetPolicyTypeName(type).ToLower())
+ </u>
+ </summary>
+ <table class="table table-striped table-hover">
+ <thead>
+ <tr>
+ <th>State key</th>
+ <th>Json contents</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var policy in invalidPolicies) {
+ <tr>
+ <td>@policy.StateKey</td>
+ <td>
+ <pre>@policy.RawContent.ToJson(true, false)</pre>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+ </details>
+ }
+
+ Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
+ Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}");
+}
+
+@if (CurrentlyEditingEvent is not null) {
+ <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal>
+}
+
+@code {
+
+#if DEBUG
+ private const bool Debug = true;
+#else
+ private const bool Debug = false;
+#endif
+
+ private bool Loading { get; set; } = true;
+
+ [Parameter]
+ public string RoomId { get; set; }
+
+ private bool _enableAvatars;
+ private StateEventResponse? _currentlyEditingEvent;
+ private StateEventResponse? _serverPolicyToMakePermanent;
+
+ private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
+
+ private StateEventResponse? CurrentlyEditingEvent {
+ get => _currentlyEditingEvent;
+ set {
+ _currentlyEditingEvent = value;
+ StateHasChanged();
+ }
+ }
+
+ private StateEventResponse? ServerPolicyToMakePermanent {
+ get => _serverPolicyToMakePermanent;
+ set {
+ _serverPolicyToMakePermanent = value;
+ StateHasChanged();
+ }
+ }
+
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; }
+ private GenericRoom Room { get; set; }
+ private RoomPowerLevelEventContent PowerLevels { get; set; }
+ private bool CurrentUserIsDraupnir { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var sw = Stopwatch.StartNew();
+ await base.OnInitializedAsync();
+ Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!;
+ if (Homeserver is null) return;
+ Room = Homeserver.GetRoom(RoomId!);
+ PowerLevels = (await Room.GetPowerLevelsAsync())!;
+ CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>("org.matrix.mjolnir.protected_rooms")) is not null;
+ await LoadStatesAsync();
+ Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
+ }
+
+ private async Task LoadStatesAsync() {
+ Loading = true;
+ var states = Room.GetFullStateAsync();
+ PolicyEventsByType.Clear();
+ await foreach (var state in states) {
+ if (state is null) continue;
+ if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
+ if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new());
+ PolicyEventsByType[state.MappedType].Add(state);
+ }
+
+ Loading = false;
+ StateHasChanged();
+ }
+
+ private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
+
+ private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+
+ private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+
+ private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
+ ?? type.GetCustomAttributes<MatrixEventAttribute>()
+ .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName;
+
+ private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
+
+ private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { });
+ PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
+ await LoadStatesAsync();
+ }
+
+ private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent);
+ CurrentlyEditingEvent = null;
+ await LoadStatesAsync();
+ }
+
+ private async Task UpgradePolicyAsync(StateEventResponse policyEvent) {
+ policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type;
+ await LoadStatesAsync();
+ }
+
+ private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+
+ // event types, unnamed
+ private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
+ .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+
+ private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes
+ .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray());
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css
new file mode 100644
index 0000000..d224737
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css
@@ -0,0 +1,32 @@
+th {
+ border-width: 1px;
+}
+
+table {
+ width: fit-content;
+ border-width: 1px;
+ vertical-align: middle;
+}
+
+.flex-grid {
+ display: grid;
+ /*grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));*/
+ /*// fit based on content max width*/
+ grid-template-columns: repeat(auto-fill, minmax(min-content, 1fr));
+
+ gap: 10px;
+}
+
+.flex-item {
+ /*flex: 1 1 30%;*/
+ /*margin: 0.25rem;*/
+ /*position: relative;*/
+ /*display: flex;*/
+ /*flex-direction: column;*/
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #fff1;
+ background-clip: border-box;
+ border: 1px solid rgba(0, 0, 0, .125);
+ border-radius: .5rem
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
new file mode 100644
index 0000000..2903ab8
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
@@ -0,0 +1,176 @@
+@page "/PolicyLists"
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes
+@using LibMatrix.EventTypes.Common
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using LibMatrix.RoomTypes
+@inject ILogger<Index> logger
+<h3>Policy lists </h3> @* <LinkButton href="/Rooms/Create">Create new policy list</LinkButton> *@
+
+@if (!string.IsNullOrWhiteSpace(Status)) {
+ <p>@Status</p>
+}
+@if (!string.IsNullOrWhiteSpace(Status2)) {
+ <p>@Status2</p>
+}
+<hr/>
+
+<table>
+ <thead>
+ <tr>
+ <th/>
+ <th>Room name</th>
+ <th>Policies</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var room in Rooms.OrderByDescending(x => x.PolicyCounts.Sum(y => y.Value))) {
+ <tr>
+ <td>
+ <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")">
+ <span class="oi oi-pencil" aria-hidden="true"></span>
+ </LinkButton>
+ </td>
+ <td style="padding-right: 24px;">
+ <span>@room.RoomName</span>
+ @if (room.IsLegacy) {
+ <span style="color: red;"> (legacy)</span>
+ }
+ <br/>
+ @if (!string.IsNullOrWhiteSpace(room.Shortcode)) {
+ <span style="font-size: 0.8em;">@room.Shortcode</span>
+ }
+ else {
+ <span style="color: red;">(no shortcode)</span>
+ }
+ </td>
+ <td>
+ <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.User) ?? 0) user policies</span><br/>
+ <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Server) ?? 0) server policies</span><br/>
+ <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Room) ?? 0) room policies</span><br/>
+ </td>
+ </tr>
+ }
+ </tbody>
+</table>
+
+@code {
+
+ private List<RoomInfo> Rooms { get; } = [];
+
+ private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (Homeserver is null) return;
+
+ Status = "Fetching rooms...";
+
+ var userEventTypes = EventContent.GetMatchingEventTypes<UserPolicyRuleEventContent>();
+ var serverEventTypes = EventContent.GetMatchingEventTypes<ServerPolicyRuleEventContent>();
+ var roomEventTypes = EventContent.GetMatchingEventTypes<RoomPolicyRuleEventContent>();
+ var knownPolicyTypes = (List<string>) [..userEventTypes, ..serverEventTypes, ..roomEventTypes];
+
+ List<GenericRoom> roomsByType = [];
+ await foreach (var room in Homeserver.GetJoinedRoomsByType("support.feline.policy.lists.msc.v1")) {
+ roomsByType.Add(room);
+ Status2 = $"Found {room.RoomId} (MSC3784)...";
+ }
+
+ List<Task<RoomInfo>> tasks = roomsByType.Select(async room => {
+ Status2 = $"Fetching room {room.RoomId}...";
+ return await RoomInfo.FromRoom(room);
+ }).ToList();
+
+ var results = tasks.ToAsyncEnumerable();
+ await foreach (var result in results) {
+ Rooms.Add(result);
+ StateHasChanged();
+ }
+
+ Status = "Searching for legacy lists...";
+
+ var rooms = (await Homeserver.GetJoinedRooms())
+ .Where(x => !Rooms.Any(y => y.Room.RoomId == x.RoomId))
+ .Select(async room => {
+ var state = await room.GetFullStateAsListAsync();
+ var policies = state
+ .Where(x => knownPolicyTypes.Contains(x.Type))
+ .ToList();
+ if (policies.Count == 0) return null;
+ Status2 = $"Found legacy list {room.RoomId}...";
+ return await RoomInfo.FromRoom(room, state, true);
+ })
+ .ToAsyncEnumerable();
+
+ await foreach (var room in rooms) {
+ if (room is not null) {
+ Rooms.Add(room);
+ StateHasChanged();
+ }
+ }
+
+ Status = "";
+ Status2 = "";
+ await base.OnInitializedAsync();
+ }
+
+ private string _status;
+
+ public string Status {
+ get => _status;
+ set {
+ _status = value;
+ StateHasChanged();
+ }
+ }
+
+ private string _status2;
+
+ public string Status2 {
+ get => _status2;
+ set {
+ _status2 = value;
+ StateHasChanged();
+ }
+ }
+
+ private class RoomInfo {
+ public GenericRoom Room { get; set; }
+ public string RoomName { get; set; }
+ public string? Shortcode { get; set; }
+ public Dictionary<PolicyType, int?> PolicyCounts { get; set; }
+ public bool IsLegacy { get; set; }
+
+ public enum PolicyType {
+ User,
+ Room,
+ Server
+ }
+
+ private static readonly List<string> userEventTypes = EventContent.GetMatchingEventTypes<UserPolicyRuleEventContent>();
+ private static readonly List<string> serverEventTypes = EventContent.GetMatchingEventTypes<ServerPolicyRuleEventContent>();
+ private static readonly List<string> roomEventTypes = EventContent.GetMatchingEventTypes<RoomPolicyRuleEventContent>();
+ private static readonly List<string> allKnownPolicyTypes = [..userEventTypes, ..serverEventTypes, ..roomEventTypes];
+
+ public static async Task<RoomInfo> FromRoom(GenericRoom room, List<StateEventResponse>? state = null, bool legacy = false) {
+ state ??= await room.GetFullStateAsListAsync();
+ return new RoomInfo() {
+ Room = room,
+ IsLegacy = legacy,
+ RoomName = await room.GetNameAsync()
+ ?? (await room.GetCanonicalAliasAsync())?.Alias
+ ?? (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode
+ ?? room.RoomId,
+ Shortcode = (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode,
+ PolicyCounts = new() {
+ { PolicyType.User, state.Count(x => userEventTypes.Contains(x.Type)) },
+ { PolicyType.Server, state.Count(x => serverEventTypes.Contains(x.Type)) },
+ { PolicyType.Room, state.Count(x => roomEventTypes.Contains(x.Type)) }
+ }
+ };
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css
new file mode 100644
index 0000000..f9b5b3f
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css
@@ -0,0 +1,6 @@
+table, th, td {
+ border-width: 1px;
+}
+td {
+ padding: 8px;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor
index 01ab1c4..46e39ed 100644
--- a/MatrixUtils.Web/Pages/Rooms/Space.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Space.razor
@@ -2,11 +2,15 @@
@using LibMatrix.RoomTypes
@using ArcaneLibs.Extensions
@using LibMatrix
+@using MatrixUtils.Abstractions
<h3>Room manager - Viewing Space</h3>
+<span>Add new room to space: </span>
+<FancyTextBox @bind-Value="@NewRoomId"></FancyTextBox>
+<button onclick="@AddNewRoom">Add</button>
<button onclick="@JoinAllRooms">Join all rooms</button>
@foreach (var room in Rooms) {
- <RoomListItem Room="room" ShowOwnProfile="true"></RoomListItem>
+ <RoomListItem RoomInfo="room" ShowOwnProfile="true"></RoomListItem>
}
@@ -27,11 +31,12 @@
private GenericRoom? Room { get; set; }
private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>();
- private List<GenericRoom> Rooms { get; } = new();
+ private List<RoomInfo> Rooms { get; } = new();
private List<string> ServersInSpace { get; } = new();
+ private string? NewRoomId { get; set; }
protected override async Task OnInitializedAsync() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
Room = hs.GetRoom(RoomId.Replace('~', '.'));
@@ -43,7 +48,18 @@
var roomId = stateEvent.StateKey;
var room = hs.GetRoom(roomId);
if (room is not null) {
- Rooms.Add(room);
+ Task.Run(async () => {
+ try {
+ Rooms.Add(new(Room, await room.GetFullStateAsListAsync()));
+ }
+ catch (MatrixException e) {
+ if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) {
+ Rooms.Add(new(Room) {
+ RoomName = "M_FORBIDDEN"
+ });
+ }
+ }
+ });
}
break;
}
@@ -96,8 +112,37 @@
// List<Task<RoomIdResponse>> tasks = Rooms.Select(room => room.JoinAsync(ServersInSpace.ToArray())).ToList();
// await Task.WhenAll(tasks);
foreach (var room in Rooms) {
+ await JoinRecursive(room.Room.RoomId);
+ }
+ }
+
+ private async Task JoinRecursive(string roomId) {
+ var room = Room!.Homeserver.GetRoom(roomId);
+ if (room is null) return;
+ try {
await room.JoinAsync(ServersInSpace.ToArray());
+ var joined = false;
+ while (!joined) {
+ var ce = await room.GetCreateEventAsync();
+ if(ce is null) continue;
+ if (ce.Type == "m.space") {
+ var children = room.AsSpace.GetChildrenAsync(false);
+ await foreach (var child in children) {
+ JoinRecursive(child.RoomId);
+ }
+ }
+ joined = true;
+ }
}
+ catch (Exception e) {
+ Console.WriteLine(e);
+ }
+
+ }
+
+ private async Task AddNewRoom() {
+ if (string.IsNullOrWhiteSpace(NewRoomId)) return;
+ await Room.AsSpace.AddChildByIdAsync(NewRoomId);
}
}
diff --git a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
index 6110b83..51cb265 100644
--- a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
+++ b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
@@ -43,7 +43,7 @@
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
RoomId = RoomId.Replace('~', '.');
await LoadStatesAsync();
@@ -53,7 +53,7 @@
private DateTime _lastUpdate = DateTime.Now;
private async Task LoadStatesAsync() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
var StateLoaded = 0;
var response = (hs.GetRoom(RoomId)).GetFullStateAsync();
diff --git a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
index 7c31136..c8b87d3 100644
--- a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
+++ b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
@@ -70,7 +70,7 @@
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
await LoadStatesAsync();
Console.WriteLine("Policy list editor initialized!");
@@ -80,7 +80,7 @@
private async Task LoadStatesAsync() {
var StateLoaded = 0;
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
var response = (hs.GetRoom(RoomId)).GetFullStateAsync();
await foreach (var _ev in response) {
diff --git a/MatrixUtils.Web/Pages/Rooms/Timeline.razor b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
index e6b1248..108581c 100644
--- a/MatrixUtils.Web/Pages/Rooms/Timeline.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
@@ -2,7 +2,7 @@
@using MatrixUtils.Web.Shared.TimelineComponents
@using LibMatrix
@using LibMatrix.EventTypes.Spec
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
<h3>RoomManagerTimeline</h3>
<hr/>
<p>Loaded @Events.Count events...</p>
@@ -27,7 +27,7 @@
protected override async Task OnInitializedAsync() {
Console.WriteLine("RoomId: " + RoomId);
- Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
var room = Homeserver.GetRoom(RoomId);
MessagesResponse? msgs = null;
diff --git a/MatrixUtils.Web/Pages/ServerInfo.razor b/MatrixUtils.Web/Pages/ServerInfo.razor
index e6f1f16..8dd7907 100644
--- a/MatrixUtils.Web/Pages/ServerInfo.razor
+++ b/MatrixUtils.Web/Pages/ServerInfo.razor
@@ -78,7 +78,7 @@
protected override async Task OnParametersSetAsync() {
if (Homeserver is not null) {
- var rhs = await hsProvider.GetRemoteHomeserver(Homeserver);
+ var rhs = await HsProvider.GetRemoteHomeserver(Homeserver);
ServerVersionResponse = await (rhs.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null));
ClientVersionsResponse = await rhs.GetClientVersionsAsync();
}
diff --git a/MatrixUtils.Web/Pages/StreamTest.razor b/MatrixUtils.Web/Pages/StreamTest.razor
new file mode 100644
index 0000000..8b9735e
--- /dev/null
+++ b/MatrixUtils.Web/Pages/StreamTest.razor
@@ -0,0 +1,119 @@
+@page "/StreamTest"
+@inject ILogger<Index> logger
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+
+<PageTitle>StreamText</PageTitle>
+@if (Homeserver is not null) {
+ <p>Got homeserver @Homeserver.ServerName</p>
+
+ @* <img src="@ResolvedUri" @ref="imgElement"/> *@
+ @* <StreamedImage Stream="@Stream"/> *@
+
+ <br/>
+ @foreach (var stream in Streams.OrderBy(x => x.GetHashCode())) {
+ <StreamedImage Stream="@stream" style="width: 12em; height: 12em; object-fit: cover;"/>
+ }
+}
+
+@code
+{
+ private string? _resolvedUri;
+
+ private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+
+ private string? ResolvedUri {
+ get => _resolvedUri;
+ set {
+ _resolvedUri = value;
+ StateHasChanged();
+ }
+ }
+
+ ElementReference imgElement { get; set; }
+ public Stream? Stream { get; set; }
+ public List<Stream> Streams { get; set; } = new();
+
+ protected override async Task OnInitializedAsync() {
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+
+ //await InitOld();
+ await Init2();
+
+ await base.OnInitializedAsync();
+ }
+
+ private async Task Init2() {
+ var roomState = await Homeserver.GetRoom("!dSMpkVKGgQHlgBDSpo:matrix.org").GetFullStateAsListAsync();
+ var members = roomState.Where(x => x.Type == RoomMemberEventContent.EventId).ToList();
+ Console.WriteLine($"Got {members.Count()} members");
+ var ss = new SemaphoreSlim(1, 1);
+ foreach (var stateEventResponse in members) {
+ // Console.WriteLine(stateEventResponse.ToJson());
+ var mc = stateEventResponse.TypedContent as RoomMemberEventContent;
+ if (!string.IsNullOrWhiteSpace(mc?.AvatarUrl)) {
+ var uri = mc.AvatarUrl[6..].Split('/');
+ var url = $"/_matrix/media/v3/download/{uri[0]}/{uri[1]}";
+ // Homeserver.GetMediaStreamAsync(mc?.AvatarUrl).ContinueWith(async x => {
+ // await ss.WaitAsync();
+ // var stream = x.Result;
+ // Streams.Add(stream);
+ // StateHasChanged();
+ await Task.Delay(100);
+ // ss.Release();
+ // });
+ try {
+ Homeserver.ClientHttpClient.GetStreamAsync(url).ContinueWith(async x => {
+ // await ss.WaitAsync();
+ var stream = x.Result;
+ Streams.Add(stream);
+ StateHasChanged();
+ // await Task.Delay(100);
+ // ss.Release();
+ });
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ }
+ }
+ }
+ }
+
+ private async Task InitOld() {
+ // var value = "mxc://rory.gay/AcFYcSpVXhEwbejrPVQrRUqt";
+ // var value = "mxc://rory.gay/oqfCjIUVTAObSQbnMFekQvYR";
+ var value = "mxc://feline.support/LUslNRVIYfeyCdRElqkkumKP";
+ var uri = value[6..].Split('/');
+ var url = $"/_matrix/media/v3/download/{uri[0]}/{uri[1]}";
+ // var res = Homeserver.ClientHttpClient.GetAsync(url);
+ // var res2 = Homeserver.ClientHttpClient.GetAsync(url);
+ // var tasks = Enumerable.Range(1, 128)
+ // .Select(x => Homeserver.ClientHttpClient.GetStreamAsync(url+$"?width={x*128}&height={x*128}"))
+ // .ToAsyncEnumerable();
+ await foreach (var result in GetStreamsDelayed(url)) {
+ Streams.Add(result);
+ // await Task.Delay(100);
+ StateHasChanged();
+ }
+
+ // var stream = await (await res).Content.ReadAsStreamAsync();
+ // Stream = await (await res2).Content.ReadAsStreamAsync();
+ StateHasChanged();
+
+ // await JSRuntime.streamImage(stream, imgElement);
+ }
+
+ private async IAsyncEnumerable<Stream> GetStreamsDelayed(string url) {
+ for (int i = 0; i < 32; i++) {
+ var tasks = Enumerable.Range(1, 4)
+ .Select(x => Homeserver.ClientHttpClient.GetStreamAsync(url + $"?width={x * 128}&height={x * 128}&r={Random.Shared.Next(100000)}"))
+ .ToAsyncEnumerable();
+ await foreach (var result in tasks) {
+ yield return result;
+ }
+ // var resp = await Homeserver.ClientHttpClient.GetAsync(url + $"?width={i * 128}&height={i * 128}");
+ // yield return await resp.Content.ReadAsStreamAsync();
+ // await Task.Delay(250);
+ }
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
index 841552e..9a56fc0 100644
--- a/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
@@ -17,7 +17,7 @@
public string? RoomId { get; set; }
protected override async Task OnInitializedAsync() {
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
Log.CollectionChanged += (sender, args) => StateHasChanged();
diff --git a/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor b/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor
index 6e87926..dd8a801 100644
--- a/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor
@@ -92,7 +92,7 @@
lines.ToList().ForEach(async line => {
await sem.WaitAsync();
try {
- homeservers.Add((await hsResolver.ResolveHomeserverFromWellKnown(line)).Client);
+ homeservers.Add((await HsResolver.ResolveHomeserverFromWellKnown(line)).Client);
StateHasChanged();
}
catch (Exception e) {
diff --git a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
index 11d35f1..0943216 100644
--- a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
@@ -39,7 +39,7 @@
private string newRoomId { get; set; }
protected override async Task OnInitializedAsync() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
StateHasChanged();
@@ -48,7 +48,7 @@
}
private async Task Execute() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
var oldRoom = hs.GetRoom(roomId);
var newRoom = hs.GetRoom(newRoomId);
@@ -90,7 +90,7 @@
private async Task TryFetchUsers() {
try {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
var room = hs.GetRoom(roomId);
var members = await room.GetMembersListAsync();
diff --git a/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor b/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor
index 263879b..7abb3d2 100644
--- a/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor
@@ -45,7 +45,7 @@
protected override async Task OnInitializedAsync() {
Status = "Getting homeserver...";
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
var syncHelper = new SyncHelper(hs) {
diff --git a/MatrixUtils.Web/Pages/Tools/Index.razor b/MatrixUtils.Web/Pages/Tools/Index.razor
index e68bb9a..f99e932 100644
--- a/MatrixUtils.Web/Pages/Tools/Index.razor
+++ b/MatrixUtils.Web/Pages/Tools/Index.razor
@@ -24,7 +24,7 @@
<a href="/Tools/Moderation/UserTrace">Trace user across rooms</a><br/>
<a href="/tools/Moderation/MassCMEBan">Mass write policies to Community Moderation Effort</a><br/>
<a href="/tools/Moderation/RoomIntersections">Find rooms with common users</a><br/>
-<a href="/tools/Moderation/DraupnirProtectedRoomsEditor">Edit Draupnir protected rooms set</a><br/>
+<a href="/tools/Moderation/Draupnir/ProtectedRoomsEditor">Draupnir: edit protected rooms set</a><br/>
<h4 class="tool-category">Debugging tools</h4>
diff --git a/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
index ddd7b15..acad827 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
@@ -1,45 +1,73 @@
-@page "/Tools/KnownHomeserverList"
+@page "/Tools/Info/KnownHomeserverList"
@using ArcaneLibs.Extensions
+@using LibMatrix.RoomTypes
+@using SpawnDev.BlazorJS.WebWorkers
+@inject WebWorkerService workerService
<h3>Known Homeserver List</h3>
<hr/>
@if (!IsFinished) {
<p>
- <b>Loading...</b>
+ <b>Loading... @RoomCount rooms remaining to process...</b>
</p>
}
-@foreach (var (homeserver, members) in counts.OrderByDescending(x => x.Value)) {
- <p>@homeserver - @members</p>
+@{
+ var shownCounts = counts.OrderByDescending(x => x.Value).AsEnumerable();
+ if (!IsFinished && counts.Count > 500) {
+ shownCounts = shownCounts.Where(x => x.Value > 5);
+ }
+}
+@foreach (var (homeserver, members) in shownCounts.ToList()) {
+ <p>@homeserver - @members users</p>
}
<hr/>
@code {
Dictionary<string, List<string>> homeservers { get; set; } = new();
+
Dictionary<string, int> counts { get; set; } = new();
+
// List<HomeserverInfo> Homeservers = new();
bool IsFinished { get; set; }
+
// HomeserverInfoQueryProgress QueryProgress { get; set; } = new();
AuthenticatedHomeserverGeneric? hs { get; set; }
+ int RoomCount { get; set; } = 0;
protected override async Task OnInitializedAsync() {
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
- var fetchTasks = (await hs.GetJoinedRooms()).Select(x=>x.GetMembersByHomeserverAsync()).ToAsyncEnumerable();
+ var ss = new SemaphoreSlim(32, 32);
+ var rooms = await hs.GetJoinedRooms();
+ RoomCount = rooms.Count;
+ var fetchTasks = rooms.Select(roomId => workerService.TaskPool.Invoke(() => InternalGetMembersByHomeserver(hs.WellKnownUris.Client, hs.AccessToken, roomId.RoomId))).ToList().ToAsyncEnumerable();
+ // var fetchTasks = rooms.Select(async x => {
+ // await ss.WaitAsync();
+ // var res = await x.GetMembersByHomeserverAsync();
+ // ss.Release();
+ // return res;
+ // }).ToAsyncEnumerable();
await foreach (var result in fetchTasks) {
foreach (var (resHomeserver, resMembers) in result) {
if (!homeservers.TryAdd(resHomeserver, resMembers)) {
homeservers[resHomeserver].AddRange(resMembers);
}
+
counts[resHomeserver] = homeservers[resHomeserver].Count;
}
- // StateHasChanged();
+
+ RoomCount--;
+ StateHasChanged();
// await Task.Delay(250);
+ await Task.Yield();
}
foreach (var resHomeserver in homeservers.Keys) {
homeservers[resHomeserver] = homeservers[resHomeserver].Distinct().ToList();
counts[resHomeserver] = homeservers[resHomeserver].Count;
+ StateHasChanged();
+ await Task.Yield();
}
IsFinished = true;
@@ -48,4 +76,10 @@
await base.OnInitializedAsync();
}
+ private static async Task<Dictionary<string, List<string>>> InternalGetMembersByHomeserver(string homeserverBaseUrl, string accessToken, string roomId) {
+ var hs = new AuthenticatedHomeserverGeneric(homeserverBaseUrl, new() { Client = homeserverBaseUrl }, null, accessToken);
+ var room = hs.GetRoom(roomId);
+ return await room.GetMembersByHomeserverAsync();
+ }
+
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
index de0bfe7..f8d1d31 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
@@ -1,24 +1,24 @@
@page "/Tools/Info/PolicyListActivity"
@using LibMatrix.EventTypes.Spec.State.Policy
@using System.Diagnostics
+@using System.Reflection
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes
@using LibMatrix.RoomTypes
@using LibMatrix.EventTypes.Common
+@using LibMatrix.Filters
-
-@if (RoomData.Count == 0)
-{
+@* <ActivityGraph Data="TestData"/> *@
+@if (RoomData.Count == 0) {
<p>Loading...</p>
}
else
- foreach (var room in RoomData)
- {
+ foreach (var room in RoomData) {
<h3>@room.Key</h3>
- @foreach (var year in room.Value.OrderBy(x => x.Key))
- {
- <h5>@year.Key</h5>
- <ActivityGraph Data="@year.Value" GlobalMax="MaxValue"
- RLabel="removed" GLabel="new" BLabel="updated policies">
- </ActivityGraph>
+ @foreach (var year in room.Value.OrderBy(x => x.Key)) {
+ <span>@year.Key</span>
+ <ActivityGraph Data="@year.Value" GlobalMax="MaxValue" RLabel="removed" GLabel="new" BLabel="updated policies"/>
}
}
@@ -29,59 +29,26 @@ else
public Dictionary<DateOnly, ActivityGraph.RGB> TestData { get; set; } = new();
- public ActivityGraph.RGB MaxValue { get; set; } = new()
- {
+ public ActivityGraph.RGB MaxValue { get; set; } = new() {
R = 255, G = 255, B = 255
};
public Dictionary<string, Dictionary<int, Dictionary<DateOnly, ActivityGraph.RGB>>> RoomData { get; set; } = new();
- protected override async Task OnInitializedAsync()
- {
+ protected override async Task OnInitializedAsync() {
var sw = Stopwatch.StartNew();
await base.OnInitializedAsync();
- Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!;
+ Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!;
if (Homeserver is null) return;
- //random test data
- for (DateOnly i = new DateOnly(2020, 1, 1); i < new DateOnly(2020, 12, 30); i = i.AddDays(Random.Shared.Next(5)))
- {
- TestData[i] = new()
- {
- R = (int)(Random.Shared.NextSingle() * 255),
- G = (int)(Random.Shared.NextSingle() * 255),
- B = (int)(Random.Shared.NextSingle() * 255)
- };
- }
-
- StateHasChanged();
- // return;
-
var rooms = await Homeserver.GetJoinedRooms();
- // foreach (var room in rooms)
- // {
- // var type = await room.GetRoomType();
- // if (type == "support.feline.policy.lists.msc.v1")
- // {
- // Console.WriteLine($"{room.RoomId} is policy list by type");
- // FilteredRooms.Add(room);
- // }
- // else if(await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null)
- // {
- // Console.WriteLine($"{room.RoomId} is policy list by shortcode");
- // FilteredRooms.Add(room);
- // }
- // }
- var roomFilterTasks = rooms.Select(async room =>
- {
+ var roomFilterTasks = rooms.Select(async room => {
var type = await room.GetRoomType();
- if (type == "support.feline.policy.lists.msc.v1")
- {
+ if (type == "support.feline.policy.lists.msc.v1") {
Console.WriteLine($"{room.RoomId} is policy list by type");
return room;
}
- else if (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null)
- {
+ else if (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null) {
Console.WriteLine($"{room.RoomId} is policy list by shortcode");
return room;
}
@@ -99,60 +66,75 @@ else
Console.WriteLine($"Filtered {FilteredRooms.Count} rooms in {sw.ElapsedMilliseconds}ms");
}
- public async Task FetchRoomHistory(GenericRoom room)
- {
+ public async Task FetchRoomHistory(GenericRoom room) {
var roomName = await room.GetNameOrFallbackAsync();
- if (string.IsNullOrWhiteSpace(roomName)) roomName = room.RoomId;
- if (!RoomData.ContainsKey(roomName))
- {
- RoomData[roomName] = new();
- }
+ if (string.IsNullOrWhiteSpace(roomName)) roomName = room.RoomId;
+ if (!RoomData.ContainsKey(roomName)) {
+ RoomData[roomName] = new();
+ }
+
+ //use timeline
+ var types = StateEventResponse.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent)));
+ var filter = new SyncFilter.EventFilter(types: types.SelectMany(x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName)).ToList());
+ var timeline = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 2500, filter: filter.ToJson(indent: false, ignoreNull: true));
+ await foreach (var response in timeline) {
+ Console.WriteLine($"Got {response.State.Count} state, {response.Chunk.Count} timeline");
+ if (response.State.Count != 0) throw new Exception("Why the hell did we receive state events?");
+ foreach (var message in response.Chunk) {
+ if (!message.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
+ //OriginServerTs to datetime
+ var dt = DateTimeOffset.FromUnixTimeMilliseconds((long)message.OriginServerTs!.Value).DateTime;
+ var date = new DateOnly(dt.Year, dt.Month, dt.Day);
+ if (!RoomData[roomName].ContainsKey(date.Year)) {
+ RoomData[roomName][date.Year] = new();
+ }
- //use timeline
- var timeline = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 5000);
- await foreach (var response in timeline)
- {
- Console.WriteLine($"Got {response.State.Count} state, {response.Chunk.Count} timeline");
- if (response.State.Count != 0) throw new Exception("Why the hell did we receive state events?");
- foreach (var message in response.Chunk)
- {
- if (!message.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
- //OriginServerTs to datetime
- var dt = DateTimeOffset.FromUnixTimeMilliseconds((long)message.OriginServerTs!.Value).DateTime;
- var date = new DateOnly(dt.Year, dt.Month, dt.Day);
- if (!RoomData[roomName].ContainsKey(date.Year))
- {
- RoomData[roomName][date.Year] = new();
- }
-
- if (!RoomData[roomName][date.Year].ContainsKey(date))
- {
- // Console.WriteLine($"Adding {date} to {roomName}");
- RoomData[roomName][date.Year][date] = new();
- }
-
- var rgb = RoomData[roomName][date.Year][date];
- if (message.RawContent?.Count == 0) rgb.R++;
- else if (string.IsNullOrWhiteSpace(message.Unsigned?.ReplacesState)) rgb.G++;
- else rgb.B++;
- RoomData[roomName][date.Year][date] = rgb;
+ if (!RoomData[roomName][date.Year].ContainsKey(date)) {
+ // Console.WriteLine($"Adding {date} to {roomName}");
+ RoomData[roomName][date.Year][date] = new();
}
- var max = RoomData.SelectMany(x => x.Value.Values).Aggregate(new ActivityGraph.RGB(), (current, next) => new()
- {
- R = Math.Max(current.R, next.Average(x => x.Value.R)),
- G = Math.Max(current.G, next.Average(x => x.Value.G)),
- B = Math.Max(current.B, next.Average(x => x.Value.B))
- });
- MaxValue = new ActivityGraph.RGB(
- r: Math.Max(max.R, Math.Max(max.G, max.B)),
- g: Math.Max(max.R, Math.Max(max.G, max.B)),
- b: Math.Max(max.R, Math.Max(max.G, max.B)));
- Console.WriteLine($"Max value is {MaxValue.R} {MaxValue.G} {MaxValue.B}");
- StateHasChanged();
- await Task.Delay(100);
+ var rgb = RoomData[roomName][date.Year][date];
+ if (message.RawContent is { Count: 0 } or null) rgb.R++;
+ else if (!message.Unsigned?.ContainsKey("replaces_state") ?? true) rgb.G++;
+ else rgb.B++;
+ RoomData[roomName][date.Year][date] = rgb;
}
+
+
+ }
+ var max = RoomData.SelectMany(x => x.Value.Values).Aggregate(new ActivityGraph.RGB(), (current, next) => new() {
+ R = Math.Max(current.R, next.Average(x => x.Value.R)),
+ G = Math.Max(current.G, next.Average(x => x.Value.G)),
+ B = Math.Max(current.B, next.Average(x => x.Value.B))
+ });
+ MaxValue = new ActivityGraph.RGB(
+ r: Math.Max(max.R, Math.Max(max.G, max.B)),
+ g: Math.Max(max.R, Math.Max(max.G, max.B)),
+ b: Math.Max(max.R, Math.Max(max.G, max.B)));
+ Console.WriteLine($"Max value is {MaxValue.R} {MaxValue.G} {MaxValue.B}");
+ StateHasChanged();
+ await Task.Yield();
}
+ private readonly struct StateEventEntry {
+ public required DateTime Timestamp { get; init; }
+ public required StateEventTransition State { get; init; }
+ public required StateEventResponse Event { get; init; }
+ public required StateEventResponse? Previous { get; init; }
+
+ public void Deconstruct(out StateEventTransition transition, out StateEventResponse evt, out StateEventResponse? prev) {
+ transition = State;
+ evt = Event;
+ prev = Previous;
+ }
+ }
+
+ private enum StateEventTransition : byte {
+ None,
+ Add,
+ Update,
+ Remove
+ }
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
index 3b68bfa..ce3513b 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
@@ -4,7 +4,7 @@
@using System.Collections.ObjectModel
@using LibMatrix
@using System.Collections.Frozen
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
<h3>User Trace</h3>
<hr/>
@@ -73,20 +73,20 @@
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
rooms.CollectionChanged += (sender, args) => StateHasChanged();
- var sessions = await RMUStorage.GetAllTokens();
+ var sessions = await sessionStore.GetAllSessions();
foreach (var userAuth in sessions) {
- var session = await RMUStorage.GetSession(userAuth);
- if (session is not null) {
- var sessionRooms = await session.GetJoinedRooms();
+ var homeserver = await sessionStore.GetHomeserver(userAuth.Key);
+ if (homeserver is not null) {
+ var sessionRooms = await homeserver.GetJoinedRooms();
foreach (var room in sessionRooms) {
rooms.Add(room);
}
StateHasChanged();
- log.Add($"Got {sessionRooms.Count} rooms for {userAuth.UserId}");
+ log.Add($"Got {sessionRooms.Count} rooms for {userAuth.Value.Auth.UserId}");
}
}
@@ -106,7 +106,7 @@
log.Add($"Done fetching members!");
- UserIDs.RemoveAll(x => sessions.Any(y => y.UserId == x));
+ UserIDs.RemoveAll(x => sessions.Any(y => y.Value.Auth.UserId == x));
StateHasChanged();
Console.WriteLine("Rerendered!");
diff --git a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
index 8f4b4dd..2313884 100644
--- a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
+++ b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
@@ -1,11 +1,8 @@
@page "/Tools/InviteCounter"
-@using ArcaneLibs.Extensions
-@using LibMatrix.RoomTypes
@using System.Collections.ObjectModel
-@using LibMatrix
-@using System.Collections.Frozen
-@using LibMatrix.EventTypes.Spec.State
-@using MatrixUtils.Abstractions
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Filters
<h3>User Trace</h3>
<hr/>
@@ -18,7 +15,7 @@
<details>
<summary>Results</summary>
- @foreach (var (userId, events) in invites.OrderByDescending(x=>x.Value).ToList()) {
+ @foreach (var (userId, events) in invites.OrderByDescending(x => x.Value).ToList()) {
<p>@userId: @events</p>
}
</details>
@@ -32,16 +29,15 @@
private ObservableCollection<string> log { get; set; } = new();
private Dictionary<string, int> invites { get; set; } = new();
private AuthenticatedHomeserverGeneric hs { get; set; }
-
+
[Parameter, SupplyParameterFromQuery(Name = "room")]
public string roomId { get; set; }
-
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
-
+
StateHasChanged();
Console.WriteLine("Rerendered!");
await base.OnInitializedAsync();
@@ -49,22 +45,21 @@
private async Task<string> Execute() {
var room = hs.GetRoom(roomId);
- var events = room.GetManyMessagesAsync(limit: int.MaxValue);
+ var filter = new SyncFilter.EventFilter(types: ["m.room.member"]);
+ var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(indent: false, ignoreNull: true));
await foreach (var resp in events) {
var all = resp.State.Concat(resp.Chunk);
foreach (var evt in all) {
- if(evt.Type != RoomMemberEventContent.EventId) continue;
+ if (evt.Type != RoomMemberEventContent.EventId) continue;
var content = evt.TypedContent as RoomMemberEventContent;
- if(content.Membership != "invite") continue;
- if(!invites.ContainsKey(evt.Sender)) invites[evt.Sender] = 0;
+ if (content.Membership != "invite") continue;
+ if (!invites.ContainsKey(evt.Sender)) invites[evt.Sender] = 0;
invites[evt.Sender]++;
}
log.Add($"{resp.State.Count} state, {resp.Chunk.Count} timeline");
}
-
-
-
+
StateHasChanged();
return "";
diff --git a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
index cbbca9e..a252e6b 100644
--- a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
+++ b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
@@ -1,12 +1,6 @@
@page "/Tools/MassCMEBan"
-@using ArcaneLibs.Extensions
-@using LibMatrix.RoomTypes
@using System.Collections.ObjectModel
-@using LibMatrix
-@using System.Collections.Frozen
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.EventTypes.Spec.State.Policy
-@using MatrixUtils.Abstractions
<h3>User Trace</h3>
<hr/>
@@ -33,7 +27,7 @@
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
StateHasChanged();
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
new file mode 100644
index 0000000..b0d5a65
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
@@ -0,0 +1,138 @@
+@page "/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/Draupnir/ProtectedRoomsEditor"
+@using LibMatrix
+@using LibMatrix.EventTypes.Interop.Draupnir
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.RoomTypes
+<h3>Edit Draupnir protected rooms</h3>
+<hr/>
+<p><b>Note:</b> You will need to restart Draupnir after applying changes!</p>
+<p>Minor note: This <i>should</i> also work with Mjolnir, but this hasn't been tested, and as such functionality cannot be guaranteed.</p>
+
+@if (data is not null) {
+ <div class="row">
+ <div class="col-12">
+ <details>
+ <summary>Currently protected room IDs</summary>
+ <ul>
+ @foreach (var room in data.Rooms) {
+ <li>@room</li>
+ }
+ </ul>
+ </details>
+ <hr/>
+ <h4>Tickyboxes</h4>
+ <table class="table">
+ <thead>
+ <tr>
+ <th></th> @* Checkbox column *@
+ <th>Kick?</th> @* PL > kick *@
+ <th>Ban?</th> @* PL > ban *@
+ <th>ACL?</th> @* PL > m.room.server_acls event *@
+ <th>Room ID</th>
+ <th>Room name</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var room in Rooms.OrderBy(x => x.RoomName)) {
+ <tr>
+ <td>
+ <input type="checkbox" @bind="room.IsProtected"/>
+ </td>
+ <td>@(room.PowerLevels.Kick <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td>
+ <td>@(room.PowerLevels.Ban <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td>
+ <td>@(room.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerAclEventContent.EventId) ? "X" : "")</td>
+ <td>@room.Room.RoomId</td>
+ <td>@room.RoomName</td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
+}
+<br/>
+<LinkButton OnClick="@Apply">Apply</LinkButton>
+
+
+@code {
+ private DraupnirProtectedRoomsData data { get; set; } = new();
+ private List<EditorRoomInfo> Rooms { get; set; } = new();
+ private AuthenticatedHomeserverGeneric hs { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is null) return;
+ data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId);
+ StateHasChanged();
+ var tasks = (await hs.GetJoinedRooms()).Select(async room => {
+ var plTask = room.GetPowerLevelsAsync();
+ var roomNameTask = room.GetNameOrFallbackAsync();
+ var EditorRoomInfo = new EditorRoomInfo {
+ Room = room,
+ IsProtected = data.Rooms.Contains(room.RoomId),
+ RoomName = await roomNameTask,
+ PowerLevels = await plTask
+ };
+
+ Rooms.Add(EditorRoomInfo);
+ StateHasChanged();
+ return Task.CompletedTask;
+ }).ToList();
+ await Task.WhenAll(tasks);
+ await Task.Delay(500);
+
+ foreach (var protectedRoomId in data.Rooms) {
+ if (Rooms.Any(x => x.Room.RoomId == protectedRoomId)) continue;
+ var room = hs.GetRoom(protectedRoomId);
+ var editorRoomInfo = new EditorRoomInfo {
+ Room = room,
+ IsProtected = true
+ };
+
+ try {
+ var pl = await room.GetPowerLevelsAsync();
+ editorRoomInfo.PowerLevels = pl;
+ }
+ catch (MatrixException e) {
+ Console.WriteLine($"Failed to get power levels for {room.RoomId}: {e}");
+ }
+
+ try {
+ editorRoomInfo.RoomName = await room.GetNameOrFallbackAsync();
+ }
+ catch (MatrixException e) {
+ Console.WriteLine($"Failed to get name for {room.RoomId}: {e}");
+ }
+
+ try {
+ var membership = await room.GetStateEventOrNullAsync(hs.UserId);
+ if (membership is not null) {
+ editorRoomInfo.RoomName = $"(!! {membership.ContentAs<RoomMemberEventContent>()?.Membership ?? "null"} !!) {editorRoomInfo.RoomName}";
+ }
+ }
+ catch (MatrixException e) {
+ Console.WriteLine($"Failed to get membership for {room.RoomId}: {e}");
+ }
+
+ Rooms.Add(editorRoomInfo);
+ }
+
+ StateHasChanged();
+ }
+
+ private class EditorRoomInfo {
+ public GenericRoom Room { get; set; }
+ public bool IsProtected { get; set; }
+ public string RoomName { get; set; }
+ public RoomPowerLevelEventContent PowerLevels { get; set; }
+ }
+
+ private async Task Apply() {
+ Console.WriteLine(string.Join('\n', Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId)));
+ data.Rooms = Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId).ToList();
+ await hs.SetAccountDataAsync("org.matrix.mjolnir.protected_rooms", data);
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
index 805bd40..ea39c9a 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
@@ -1,7 +1,7 @@
-@page "/Moderation/DraupnirProtectedRoomsEditor"
-@page "/Tools/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/Draupnir/ProtectionsEditor"
@using System.Text.Json.Serialization
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.RoomTypes
<h3>Edit Draupnir protected rooms</h3>
<hr/>
@@ -38,7 +38,7 @@
</td>
<td>@(room.PowerLevels.Kick <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td>
<td>@(room.PowerLevels.Ban <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td>
- <td>@(room.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerACLEventContent.EventId) ? "X" : "")</td>
+ <td>@(room.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerAclEventContent.EventId) ? "X" : "")</td>
<td>@room.Room.RoomId</td>
<td>@room.RoomName</td>
</tr>
@@ -58,7 +58,7 @@
private AuthenticatedHomeserverGeneric hs { get; set; }
protected override async Task OnInitializedAsync() {
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>("org.matrix.mjolnir.protected_rooms");
StateHasChanged();
@@ -78,6 +78,43 @@
}).ToList();
await Task.WhenAll(tasks);
await Task.Delay(500);
+
+ foreach (var protectedRoomId in data.Rooms) {
+ if (Rooms.Any(x => x.Room.RoomId == protectedRoomId)) continue;
+ var room = hs.GetRoom(protectedRoomId);
+ var editorRoomInfo = new EditorRoomInfo {
+ Room = room,
+ IsProtected = true
+ };
+
+ try {
+ var pl = await room.GetPowerLevelsAsync();
+ editorRoomInfo.PowerLevels = pl;
+ }
+ catch (MatrixException e) {
+ Console.WriteLine($"Failed to get power levels for {room.RoomId}: {e}");
+ }
+
+ try {
+ editorRoomInfo.RoomName = await room.GetNameOrFallbackAsync();
+ }
+ catch (MatrixException e) {
+ Console.WriteLine($"Failed to get name for {room.RoomId}: {e}");
+ }
+
+ try {
+ var membership = await room.GetStateEventOrNullAsync(hs.UserId);
+ if (membership is not null) {
+ editorRoomInfo.RoomName = $"(!! {membership.ContentAs<RoomMemberEventContent>()?.Membership ?? "null"} !!) {editorRoomInfo.RoomName}";
+ }
+ }
+ catch (MatrixException e) {
+ Console.WriteLine($"Failed to get membership for {room.RoomId}: {e}");
+ }
+
+ Rooms.Add(editorRoomInfo);
+ }
+
StateHasChanged();
}
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
new file mode 100644
index 0000000..9e70687
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
@@ -0,0 +1,139 @@
+@page "/Tools/Moderation/Draupnir/WatchedListsEditor"
+@using System.Text.Json.Serialization
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.RoomTypes
+<h3>Edit Draupnir protected rooms</h3>
+<hr/>
+<p><b>Note:</b> You will need to restart Draupnir after applying changes!</p>
+<p>Minor note: This <i>should</i> also work with Mjolnir, but this hasn't been tested, and as such functionality cannot be guaranteed.</p>
+
+@if (data is not null) {
+ <div class="row">
+ <div class="col-12">
+ <h4>Current rooms</h4>
+ <ul>
+ @foreach (var room in data.Rooms) {
+ <li>@room</li>
+ }
+ </ul>
+ <hr/>
+ <h4>Tickyboxes</h4>
+ <table class="table">
+ <thead>
+ <tr>
+ <th></th> @* Checkbox column *@
+ <th>Kick?</th> @* PL > kick *@
+ <th>Ban?</th> @* PL > ban *@
+ <th>ACL?</th> @* PL > m.room.server_acls event *@
+ <th>Room ID</th>
+ <th>Room name</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var room in Rooms.OrderBy(x => x.RoomName)) {
+ <tr>
+ <td>
+ <input type="checkbox" @bind="room.IsProtected"/>
+ </td>
+ <td>@(room.PowerLevels.Kick <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td>
+ <td>@(room.PowerLevels.Ban <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td>
+ <td>@(room.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerAclEventContent.EventId) ? "X" : "")</td>
+ <td>@room.Room.RoomId</td>
+ <td>@room.RoomName</td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
+}
+<br/>
+<LinkButton OnClick="@Apply">Apply</LinkButton>
+
+
+@code {
+ private DraupnirProtectedRoomsData data { get; set; } = new();
+ private List<EditorRoomInfo> Rooms { get; set; } = new();
+ private AuthenticatedHomeserverGeneric hs { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is null) return;
+ data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>("org.matrix.mjolnir.protected_rooms");
+ StateHasChanged();
+ var tasks = (await hs.GetJoinedRooms()).Select(async room => {
+ var plTask = room.GetPowerLevelsAsync();
+ var roomNameTask = room.GetNameOrFallbackAsync();
+ var EditorRoomInfo = new EditorRoomInfo {
+ Room = room,
+ IsProtected = data.Rooms.Contains(room.RoomId),
+ RoomName = await roomNameTask,
+ PowerLevels = await plTask
+ };
+
+ Rooms.Add(EditorRoomInfo);
+ StateHasChanged();
+ return Task.CompletedTask;
+ }).ToList();
+ await Task.WhenAll(tasks);
+ await Task.Delay(500);
+
+ foreach (var protectedRoomId in data.Rooms) {
+ if (Rooms.Any(x => x.Room.RoomId == protectedRoomId)) continue;
+ var room = hs.GetRoom(protectedRoomId);
+ var editorRoomInfo = new EditorRoomInfo {
+ Room = room,
+ IsProtected = true
+ };
+
+ try {
+ var pl = await room.GetPowerLevelsAsync();
+ editorRoomInfo.PowerLevels = pl;
+ }
+ catch (MatrixException e) {
+ Console.WriteLine($"Failed to get power levels for {room.RoomId}: {e}");
+ }
+
+ try {
+ editorRoomInfo.RoomName = await room.GetNameOrFallbackAsync();
+ }
+ catch (MatrixException e) {
+ Console.WriteLine($"Failed to get name for {room.RoomId}: {e}");
+ }
+
+ try {
+ var membership = await room.GetStateEventOrNullAsync(hs.UserId);
+ if (membership is not null) {
+ editorRoomInfo.RoomName = $"(!! {membership.ContentAs<RoomMemberEventContent>()?.Membership ?? "null"} !!) {editorRoomInfo.RoomName}";
+ }
+ }
+ catch (MatrixException e) {
+ Console.WriteLine($"Failed to get membership for {room.RoomId}: {e}");
+ }
+
+ Rooms.Add(editorRoomInfo);
+ }
+
+ StateHasChanged();
+ }
+
+ private class DraupnirProtectedRoomsData {
+ [JsonPropertyName("rooms")]
+ public List<string> Rooms { get; set; } = new();
+ }
+
+ private class EditorRoomInfo {
+ public GenericRoom Room { get; set; }
+ public bool IsProtected { get; set; }
+ public string RoomName { get; set; }
+ public RoomPowerLevelEventContent PowerLevels { get; set; }
+ }
+
+ private async Task Apply() {
+ Console.WriteLine(string.Join('\n', Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId)));
+ data.Rooms = Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId).ToList();
+ await hs.SetAccountDataAsync("org.matrix.mjolnir.protected_rooms", data);
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
new file mode 100644
index 0000000..b62cf57
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
@@ -0,0 +1,192 @@
+@page "/Tools/Moderation/FindUsersByRegex"
+@using System.Collections.Frozen
+@using ArcaneLibs.Extensions
+@using LibMatrix.RoomTypes
+@using System.Collections.ObjectModel
+@using System.Text.RegularExpressions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Filters
+@using LibMatrix.Helpers
+<h3>Find users by regex</h3>
+<hr/>
+
+<p>Users (regex): </p>
+<InputTextArea @bind-Value="@UserIdString"></InputTextArea>
+
+<LinkButton OnClick="Execute">Execute</LinkButton>
+<br/>
+<LinkButton OnClick="RemoveKicks">Remove kicks</LinkButton>
+<LinkButton OnClick="RemoveBans">Remove bans</LinkButton>
+<br/>
+
+
+<details>
+ <summary>Results</summary>
+ @foreach (var (userId, events) in matches) {
+ <h4>@userId</h4>
+ <ul>
+ @foreach (var match in events) {
+ <li>
+ <ul>
+ <li>@match.RoomName (<span>@match.Room.RoomId</span>)</li>
+ <li>Membership: @(match.Event.RawContent.ToJson(indent: false)) (sent by @match.Event.Sender)</li>
+ </ul>
+ </li>
+ }
+ </ul>
+ }
+</details>
+
+<br/>
+@foreach (var line in log.Reverse()) {
+ <pre>@line</pre>
+}
+
+@code {
+
+ private ObservableCollection<string> log { get; set; } = new();
+
+ // List<RoomInfo> rooms { get; set; } = new();
+ List<GenericRoom> rooms { get; set; } = [];
+ Dictionary<string, List<Match>> matches = new();
+
+ private string UserIdString {
+ get => string.Join("\n", UserIDs);
+ set => UserIDs = value.Split("\n").Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
+ }
+
+ private List<string> UserIDs { get; set; } = new();
+
+ private AuthenticatedHomeserverGeneric hs { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ log.CollectionChanged += (sender, args) => StateHasChanged();
+ log.Add("Authenticating");
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is null) return;
+
+ StateHasChanged();
+ Console.WriteLine("Rerendered!");
+ await base.OnInitializedAsync();
+ }
+
+ private async Task<string> Execute() {
+ log.Add("Constructing sync helper...");
+ var sh = new SyncHelper(hs) {
+ Filter = new SyncFilter() {
+ AccountData = new(types: []),
+ Presence = new(types: []),
+ Room = new() {
+ AccountData = new(types: []),
+ Ephemeral = new(types: []),
+ State = new(types: [RoomMemberEventContent.EventId]),
+ Timeline = new(types: []),
+ IncludeLeave = false
+ },
+ }
+ };
+
+ log.Add("Starting sync...");
+ var res = await sh.SyncAsync();
+
+ log.Add("Got sync response, parsing...");
+
+ var roomNames = (await Task.WhenAll((await hs.GetJoinedRooms()).Select(async room => { return (room.RoomId, await room.GetNameOrFallbackAsync()); }).ToList())).ToFrozenDictionary(x => x.Item1, x => x.Item2);
+
+ foreach (var userIdRegex in UserIDs) {
+ var regex = new Regex(userIdRegex, RegexOptions.Compiled);
+ log.Add($"Searching for {regex}:");
+ foreach (var (roomId, joinedRoom) in res.Rooms.Join) {
+ log.Add($"- Checking room {roomId}...");
+ foreach (var evt in joinedRoom.State.Events) {
+ if (evt.StateKey is null) continue;
+ if (evt.Type is not RoomMemberEventContent.EventId) continue;
+
+ if (regex.IsMatch(evt.StateKey)) {
+ log.Add($" - Found match in {roomId} for {evt.StateKey}");
+ if (!matches.ContainsKey(evt.StateKey)) {
+ matches[evt.StateKey] = new();
+ }
+
+ var room = hs.GetRoom(roomId);
+ matches[evt.StateKey].Add(new Match {
+ Room = room,
+ Event = evt,
+ RoomName = roomNames[roomId]
+ });
+ }
+ }
+ }
+ }
+
+ log.Add("Done!");
+
+ StateHasChanged();
+
+ return "";
+ }
+
+ public string? ImportFromRoomId { get; set; }
+
+ private async Task DoImportFromRoomId() {
+ try {
+ if (ImportFromRoomId is null) return;
+ var room = rooms.FirstOrDefault(x => x.RoomId == ImportFromRoomId);
+ UserIdString = string.Join("\n", (await room.GetMembersListAsync()).Select(x => x.StateKey));
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ log.Add("Could not fetch members list!\n" + e.ToString());
+ }
+
+ StateHasChanged();
+ }
+
+ private class Match {
+ public GenericRoom Room;
+ public StateEventResponse Event;
+ public string RoomName { get; set; }
+ }
+
+ private async IAsyncEnumerable<Match> GetMatches(string userId) {
+ var results = rooms.Select(async room => {
+ var state = await room.GetStateEventOrNullAsync(room.RoomId, userId);
+ if (state is not null) {
+ return new Match {
+ Room = room,
+ Event = state,
+ RoomName = await room.GetNameOrFallbackAsync()
+ };
+ }
+
+ return null;
+ }).ToAsyncEnumerable();
+ await foreach (var result in results) {
+ if (result is not null) {
+ yield return result;
+ }
+ }
+ }
+
+ private Task RemoveKicks() {
+ foreach (var (userId, matches) in matches) {
+ matches.RemoveAll(x => x.Event.ContentAs<RoomMemberEventContent>()!.Membership == "leave" && x.Event.Sender != x.Event.StateKey);
+ }
+
+ matches.RemoveAll((x, y) => y.Count == 0);
+ StateHasChanged();
+ return Task.CompletedTask;
+ }
+
+ private Task RemoveBans() {
+ foreach (var (userId, matches) in matches) {
+ matches.RemoveAll(x => x.Event.ContentAs<RoomMemberEventContent>()!.Membership == "ban" && x.Event.Sender != x.Event.StateKey);
+ }
+
+ matches.RemoveAll((x, y) => y.Count == 0);
+ StateHasChanged();
+ return Task.CompletedTask;
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
index 2123d4d..5c5946f 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
@@ -1,6 +1,8 @@
@page "/Tools/Moderation/InviteCounter"
@using System.Collections.ObjectModel
-@using LibMatrix.EventTypes.Spec.State
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Filters
<h3>Invite counter</h3>
<hr/>
@@ -13,7 +15,7 @@
<details>
<summary>Results</summary>
- @foreach (var (userId, events) in invites.OrderByDescending(x=>x.Value).ToList()) {
+ @foreach (var (userId, events) in invites.OrderByDescending(x => x.Value).ToList()) {
<p>@userId: @events</p>
}
</details>
@@ -27,16 +29,15 @@
private ObservableCollection<string> log { get; set; } = new();
private Dictionary<string, int> invites { get; set; } = new();
private AuthenticatedHomeserverGeneric hs { get; set; }
-
+
[Parameter, SupplyParameterFromQuery(Name = "room")]
public string roomId { get; set; }
-
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
-
+
StateHasChanged();
Console.WriteLine("Rerendered!");
await base.OnInitializedAsync();
@@ -44,22 +45,21 @@
private async Task<string> Execute() {
var room = hs.GetRoom(roomId);
- var events = room.GetManyMessagesAsync(limit: int.MaxValue);
+ var filter = new SyncFilter.EventFilter() { Types = [RoomMemberEventContent.EventId] };
+ var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(ignoreNull: true, indent: false));
await foreach (var resp in events) {
var all = resp.State.Concat(resp.Chunk);
foreach (var evt in all) {
- if(evt.Type != RoomMemberEventContent.EventId) continue;
+ if (evt.Type != RoomMemberEventContent.EventId) continue;
var content = evt.TypedContent as RoomMemberEventContent;
- if(content.Membership != "invite") continue;
- if(!invites.ContainsKey(evt.Sender)) invites[evt.Sender] = 0;
- invites[evt.Sender]++;
+ if (content?.Membership != "invite") continue;
+ invites.TryAdd(evt.Sender!, 0);
+ invites[evt.Sender!]++;
}
log.Add($"{resp.State.Count} state, {resp.Chunk.Count} timeline");
}
-
-
-
+
StateHasChanged();
return "";
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
index ea1e5f6..8fdad84 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -1,6 +1,7 @@
@page "/Tools/Moderation/MassCMEBan"
@using System.Collections.ObjectModel
@using LibMatrix.EventTypes.Spec.State.Policy
+@using LibMatrix.RoomTypes
<h3>User Trace</h3>
<hr/>
@@ -17,19 +18,19 @@
}
@code {
+
// TODO: Properly implement page to be more useful
private ObservableCollection<string> log { get; set; } = new();
private AuthenticatedHomeserverGeneric hs { get; set; }
-
+
[Parameter, SupplyParameterFromQuery(Name = "room")]
public string roomId { get; set; }
-
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
-
+
StateHasChanged();
Console.WriteLine("Rerendered!");
await base.OnInitializedAsync();
@@ -37,33 +38,41 @@
private async Task<string> Execute() {
var room = hs.GetRoom("!fTjMjIzNKEsFlUIiru:neko.dev");
- // var room = hs.GetRoom("!yf7OpOiRDXx6zUGpT6:conduit.rory.gay");
- var users = roomId.Split("\n").Select(x => x.Trim()).Where(x=>x.StartsWith('@')).ToList();
- foreach (var user in users) {
- var exists = false;
- try {
- exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity);
- } catch (Exception e) {
- log.Add($"Failed to get {user}");
- }
+ // var room = hs.GetRoom("!IVSjKMsVbjXsmUTuRR:rory.gay");
+ var users = roomId.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList();
+ var tasks = users.Select(x => ExecuteBan(room, x)).ToList();
+ await Task.WhenAll(tasks);
+
+ StateHasChanged();
+
+ return "";
+ }
- if (!exists) {
+ private async Task ExecuteBan(GenericRoom room, string user) {
+ var exists = false;
+ try {
+ exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity);
+ }
+ catch (Exception e) {
+ log.Add($"Failed to get {user}");
+ }
+
+ if (!exists) {
+ try {
var evt = await room.SendStateEventAsync(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'), new UserPolicyRuleEventContent() {
Entity = user,
- Reason = "spam (invite)",
+ Reason = "spam",
Recommendation = "m.ban"
});
log.Add($"Sent {evt.EventId} to ban {user}");
}
- else {
- log.Add($"User {user} already exists");
+ catch (Exception e) {
+ log.Add($"Failed to ban {user}: {e}");
}
}
-
-
- StateHasChanged();
-
- return "";
+ else {
+ log.Add($"User {user} already exists");
+ }
}
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index e5ba004..1ec3cd0 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -1,31 +1,162 @@
@page "/Tools/Moderation/MembershipHistory"
+@using System.Collections.Frozen
@using System.Collections.ObjectModel
+@using System.Diagnostics
+@using System.Text.Json
+@using ArcaneLibs.Extensions
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Filters
+@{
+ var sw = Stopwatch.StartNew();
+ Console.WriteLine("Start render");
+}
<h3>Membership history viewer</h3>
<hr/>
-
<br/>
<span>Room ID: </span>
-<InputText @bind-Value="@roomId"></InputText>
+<InputText @bind-Value="@RoomId"></InputText>
<LinkButton OnClick="@Execute">Execute</LinkButton>
-<p><InputCheckbox @bind-Value="ChronologicalOrder"/> Chronological order</p>
+<p>
+ <span><InputCheckbox @bind-Value="ChronologicalOrder"/>Chronological order</span>
+ <span><InputCheckbox @bind-Value="DoDisambiguate"/>Enable extended filters</span>
+</p>
<p>
<span>Show </span>
- <InputCheckbox @bind-Value="ShowJoins"/> joins
- <InputCheckbox @bind-Value="ShowLeaves"/> leaves
- <InputCheckbox @bind-Value="ShowUpdates"/> profile updates
- <InputCheckbox @bind-Value="ShowKnocks"/> knocks
- <InputCheckbox @bind-Value="ShowInvites"/> invites
- <InputCheckbox @bind-Value="ShowKicks"/> kicks
- <InputCheckbox @bind-Value="ShowBans"/> bans
+ <span><InputCheckbox @bind-Value="ShowJoins"/> joins</span>
+ <span><InputCheckbox @bind-Value="ShowLeaves"/> leaves</span>
+ <span><InputCheckbox @bind-Value="ShowKnocks"/> knocks</span>
+ <span><InputCheckbox @bind-Value="ShowInvites"/> invites</span>
+ <span><InputCheckbox @bind-Value="ShowBans"/> bans</span>
</p>
<p>
- <LinkButton OnClick="@(async () => { ShowJoins = ShowLeaves = ShowUpdates = ShowKnocks = ShowInvites = ShowKicks = ShowBans = false; })">Hide all</LinkButton>
- <LinkButton OnClick="@(async () => { ShowJoins = ShowLeaves = ShowUpdates = ShowKnocks = ShowInvites = ShowKicks = ShowBans = true; })">Show all</LinkButton>
- <LinkButton OnClick="@(async () => { ShowJoins ^= true; ShowLeaves ^= true; ShowUpdates ^= true; ShowKnocks ^= true; ShowInvites ^= true; ShowKicks ^= true; ShowBans ^= true; })">Toggle all</LinkButton>
+ <LinkButton OnClick="@(async () => {
+ ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false;
+ StateHasChanged();
+ })">Hide all
+ </LinkButton>
+ <LinkButton OnClick="@(async () => {
+ ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true;
+ StateHasChanged();
+ })">Show all
+ </LinkButton>
+ <LinkButton OnClick="@(async () => {
+ ShowJoins ^= true;
+ ShowLeaves ^= true;
+ ShowKnocks ^= true;
+ ShowInvites ^= true;
+ ShowBans ^= true;
+ StateHasChanged();
+ })">Toggle all
+ </LinkButton>
</p>
<p>
+ <span><InputCheckbox @bind-Value="DoDisambiguate"/> Disambiguate </span>
+ @if (DoDisambiguate) {
+ <span><InputCheckbox @bind-Value="DisambiguateKicks"/> kicks</span>
+ <span><InputCheckbox @bind-Value="DisambiguateUnbans"/> unbans</span>
+ <span><InputCheckbox @bind-Value="DisambiguateProfileUpdates"/> profile updates</span>
+ <details style="display: inline-block; vertical-align: top;">
+ <summary>
+ <InputCheckbox @bind-Value="DisambiguateInviteActions"/>
+ invite actions
+ </summary>
+ <span><InputCheckbox @bind-Value="DisambiguateInviteAccepted"/> accepted</span>
+ <span><InputCheckbox @bind-Value="DisambiguateInviteRejected"/> rejected</span>
+ <span><InputCheckbox @bind-Value="DisambiguateInviteRetracted"/> retracted</span>
+ </details>
+ <details style="display: inline-block; vertical-align: top;">
+ <summary>
+ <InputCheckbox @bind-Value="DisambiguateKnockActions"/>
+ knock actions
+ </summary>
+ <span><InputCheckbox @bind-Value="DisambiguateKnockAccepted"/> accepted</span>
+ <span><InputCheckbox @bind-Value="DisambiguateKnockRejected"/> rejected</span>
+ <span><InputCheckbox @bind-Value="DisambiguateKnockRetracted"/> retracted</span>
+ </details>
+ }
+</p>
+@if (DoDisambiguate) {
+ <p>
+ <span>Show </span>
+ @if (DisambiguateKicks) {
+ <span><InputCheckbox @bind-Value="ShowKicks"/> kicks</span>
+ }
+ @if (DisambiguateUnbans) {
+ <span><InputCheckbox @bind-Value="ShowUnbans"/> unbans</span>
+ }
+ @if (DisambiguateProfileUpdates) {
+ <span><InputCheckbox @bind-Value="ShowProfileUpdates"/> profile updates</span>
+ }
+ @if (DisambiguateInviteActions) {
+ <details style="display: inline-block; vertical-align: top;">
+ <summary>
+ <InputCheckbox @bind-Value="ShowInviteActions"/>
+ invite actions
+ </summary>
+ @if (DisambiguateInviteAccepted) {
+ <span><InputCheckbox @bind-Value="ShowInviteAccepted"/> accepted</span>
+ }
+
+ @if (DisambiguateInviteRejected) {
+ <span><InputCheckbox @bind-Value="ShowInviteRejected"/> rejected</span>
+ }
+
+ @if (DisambiguateInviteRetracted) {
+ <span><InputCheckbox @bind-Value="ShowInviteRetracted"/> retracted</span>
+ }
+ </details>
+ }
+ @if (DisambiguateKnockActions) {
+ <details style="display: inline-block; vertical-align: top;">
+ <summary>
+ <InputCheckbox @bind-Value="ShowKnockActions"/>
+ knock actions
+ </summary>
+ @if (DisambiguateKnockAccepted) {
+ <span><InputCheckbox @bind-Value="ShowKnockAccepted"/> accepted</span>
+ }
+
+ @if (DisambiguateKnockRejected) {
+ <span><InputCheckbox @bind-Value="ShowKnockRejected"/> rejected</span>
+ }
+
+ @if (DisambiguateKnockRetracted) {
+ <span><InputCheckbox @bind-Value="ShowKnockRetracted"/> retracted</span>
+ }
+ </details>
+ }
+ </p>
+
+ <p>
+ <LinkButton OnClick="@(async () => {
+ DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false;
+ StateHasChanged();
+ })">Un-disambiguate all
+ </LinkButton>
+ <LinkButton OnClick="@(async () => {
+ DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true;
+ StateHasChanged();
+ })">Disambiguate all
+ </LinkButton>
+ <LinkButton OnClick="@(async () => {
+ DisambiguateProfileUpdates ^= true;
+ DisambiguateKicks ^= true;
+ DisambiguateUnbans ^= true;
+ DisambiguateInviteAccepted ^= true;
+ DisambiguateInviteRejected ^= true;
+ DisambiguateInviteRetracted ^= true;
+ DisambiguateKnockAccepted ^= true;
+ DisambiguateKnockRejected ^= true;
+ DisambiguateKnockRetracted ^= true;
+ DisambiguateKnockActions ^= true;
+ DisambiguateInviteActions ^= true;
+ StateHasChanged();
+ })">Toggle all
+ </LinkButton>
+ </p>
+}
+<p>
<span>Sender: </span>
<InputSelect @bind-Value="Sender">
<option value="">All</option>
@@ -44,92 +175,121 @@
</InputSelect>
</p>
-
+@{ Console.WriteLine($"Rendering took {sw.Elapsed} for {Memberships.Count} items"); }
<br/>
-<details>
+<details open>
<summary>Results</summary>
@{
- Dictionary<string, StateEventResponse> previousMemberships = [];
- var filteredMemberships = Memberships.AsEnumerable();
- if (ChronologicalOrder) {
- filteredMemberships = filteredMemberships.Reverse();
- }
- if(!string.IsNullOrWhiteSpace(Sender)) {
- filteredMemberships = filteredMemberships.Where(x => x.Sender == Sender);
- }
- if(!string.IsNullOrWhiteSpace(User)) {
- filteredMemberships = filteredMemberships.Where(x => x.StateKey == User);
- }
-
- @foreach (var membership in filteredMemberships) {
- RoomMemberEventContent content = membership.TypedContent as RoomMemberEventContent;
- @switch (content.Membership) {
- case RoomMemberEventContent.MembershipTypes.Invite: {
- if (_showInvites) {
- <p style="color: green;">@membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
- }
-
- break;
- }
- case RoomMemberEventContent.MembershipTypes.Ban: {
- if (_showBans) {
- <p style="color: red;">@membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
- }
-
- break;
- }
- case RoomMemberEventContent.MembershipTypes.Leave: {
- if (membership.Sender == membership.StateKey) {
- if (_showLeaves) {
- <p style="color: #C66;">@membership.Sender left the room</p>
- }
- }
- else {
- if (_showKicks) {
- <p style="color: darkorange;">@membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
- }
- }
-
- break;
- }
- case RoomMemberEventContent.MembershipTypes.Knock: {
- if (_showKnocks) {
- <p>@membership.Sender knocked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
- }
-
- break;
- }
- case RoomMemberEventContent.MembershipTypes.Join: {
- if (previousMemberships.TryGetValue(membership.StateKey, out var previous)
- && (previous.TypedContent as RoomMemberEventContent).Membership == RoomMemberEventContent.MembershipTypes.Join) {
- if (_showUpdates) {
- <p style="color: #777;">@membership.Sender changed their profile</p>
- }
- }
- else {
- if (_showJoins) {
- <p style="color: #6C6;">@membership.Sender joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
- }
+ var filteredMemberships = GetFilteredMemberships();
+ }
+ <table>
+ @foreach (var membershipEntry in filteredMemberships) {
+ var (transition, membership, previousMembership) = membershipEntry;
+ RoomMemberEventContent content = membership.TypedContent as RoomMemberEventContent ?? throw new InvalidOperationException("Event is not a RoomMemberEventContent!");
+ RoomMemberEventContent? previousContent = previousMembership?.TypedContent as RoomMemberEventContent;
+
+ <tr>
+ <td>@DateTimeOffset.FromUnixTimeMilliseconds(membership.OriginServerTs ?? 0).ToString("g")</td>
+ <td>
+ @switch (transition) {
+ case MembershipTransition.None:
+ <b>Unknown membership! Got None</b>
+ break;
+ case MembershipTransition.Join:
+ <p style="color: #6C6;">
+ @membership.StateKey joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")<br/>
+ Display name: @content.DisplayName<br/>
+ Avatar URL: @content.AvatarUrl
+ </p>
+ break;
+ case MembershipTransition.Leave:
+ <p style="color: #C66;">
+ @membership.StateKey left the room
+ </p>
+ break;
+ case MembershipTransition.Knock:
+ <p style="color: #426">
+ @membership.StateKey knocked @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.Invite:
+ <p style="color: #262;">
+ @membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.Ban:
+ <p style="color: red;">
+ @membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ @* disambiguated *@
+ case MembershipTransition.Kick:
+ <p style="color: darkorange;">
+ @membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.ProfileUpdate:
+ <p style="color: #777;">
+ @membership.Sender changed their profile<br/>
+ Display name: @previousContent!.DisplayName -> @content.DisplayName<br/>
+ Avatar URL: @previousContent.AvatarUrl -> @content.AvatarUrl
+ </p>
+ break;
+ case MembershipTransition.InviteAccepted:
+ <p style="color: #084;">
+ @membership.StateKey accepted the invite
+ from @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(invite reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(accept reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.KnockAccepted:
+ <p style="color: #288;">
+ @membership.StateKey's knock was accepted
+ by @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(knock reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(accept reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.KnockRejected:
+ <p style="color: #828;">
+ @membership.StateKey's knock was rejected
+ by @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(knock reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reject reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.Unban:
+ <p style="color: #0C0;">
+ @membership.Sender unbanned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.InviteRejected:
+ <p style="color: #733;">
+ @membership.StateKey rejected the invite
+ from @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(invite reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reject reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.InviteRetracted:
+ <p style="color: #844;">
+ @membership.Sender retracted the invite
+ for @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.KnockRetracted:
+ <p style="color: #b55;">
+ @membership.Sender retracted the knock
+ for @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
}
-
- break;
- }
- default: {
- <b>Unknown membership @content.Membership!</b>
- break;
- }
- }
-
- previousMemberships[membership.StateKey] = membership;
+ </td>
+ </tr>
}
- }
+ </table>
</details>
<br/>
<details open>
<summary>Log</summary>
- @foreach (var line in log.Reverse()) {
+ @foreach (var line in Log.Reverse()) {
<pre>@line</pre>
}
</details>
@@ -138,139 +298,289 @@
#region Filter bindings
- private bool _chronologicalOrder = false;
-
- private bool ChronologicalOrder {
- get => _chronologicalOrder;
- set {
- _chronologicalOrder = value;
- StateHasChanged();
- }
- }
+ private bool ChronologicalOrder { get; set; }
+ private bool ShowJoins { get; set; } = true;
+ private bool ShowLeaves { get; set; } = true;
+ private bool ShowKnocks { get; set; } = true;
+ private bool ShowInvites { get; set; } = true;
+ private bool ShowBans { get; set; } = true;
+
+ private bool DoDisambiguate { get; set; } = true;
+ private bool DisambiguateProfileUpdates { get => field && DoDisambiguate; set; } = true;
+ private bool DisambiguateKicks { get => field && DoDisambiguate; set; } = true;
+ private bool DisambiguateUnbans { get => field && DoDisambiguate; set; } = true;
+ private bool DisambiguateInviteAccepted { get => field && DoDisambiguate && DisambiguateInviteActions; set; } = true;
+ private bool DisambiguateInviteRejected { get => field && DoDisambiguate && DisambiguateInviteActions; set; } = true;
+ private bool DisambiguateInviteRetracted { get => field && DoDisambiguate && DisambiguateInviteActions; set; } = true;
+ private bool DisambiguateKnockAccepted { get => field && DoDisambiguate && DisambiguateKnockActions; set; } = true;
+ private bool DisambiguateKnockRejected { get => field && DoDisambiguate && DisambiguateKnockActions; set; } = true;
+ private bool DisambiguateKnockRetracted { get => field && DoDisambiguate && DisambiguateKnockActions; set; } = true;
+
+ private bool DisambiguateKnockActions { get => field && DoDisambiguate; set; } = true;
+ private bool DisambiguateInviteActions { get => field && DoDisambiguate; set; } = true;
- private bool _showJoins = true;
+ private bool ShowProfileUpdates {
+ get => field && DisambiguateProfileUpdates;
+ set;
+ } = true;
- private bool ShowJoins {
- get => _showJoins;
+ private bool ShowKicks {
+ get => field && DisambiguateKicks;
+ set;
+ } = true;
+
+ private bool ShowUnbans {
+ get => field && DisambiguateUnbans;
+ set;
+ } = true;
+
+ private bool ShowInviteAccepted {
+ get => field && DisambiguateInviteAccepted;
+ set;
+ } = true;
+
+ private bool ShowInviteRejected {
+ get => field && DisambiguateInviteRejected;
+ set;
+ } = true;
+
+ private bool ShowInviteRetracted {
+ get => field && DisambiguateInviteRetracted;
+ set;
+ } = true;
+
+ private bool ShowKnockAccepted {
+ get => field && DisambiguateKnockAccepted;
+ set;
+ } = true;
+
+ private bool ShowKnockRejected {
+ get => field && DisambiguateKnockRejected;
+ set;
+ } = true;
+
+ private bool ShowKnockRetracted {
+ get => field && DisambiguateKnockRetracted;
+ set;
+ } = true;
+
+ private bool ShowKnockActions {
+ get => field && DisambiguateKnockActions;
+ set;
+ } = true;
+
+ private bool ShowInviteActions {
+ get => field && DisambiguateInviteActions;
+ set;
+ } = true;
+
+ [Parameter, SupplyParameterFromQuery(Name = "sender")]
+ public string Sender { get; set; } = "";
+
+ [Parameter, SupplyParameterFromQuery(Name = "user")]
+ public string User { get; set; } = "";
+
+ [Parameter, SupplyParameterFromQuery(Name = "filter")]
+ public string Filter {
+ get;
set {
- _showJoins = value;
+ field = value;
+ if (string.IsNullOrWhiteSpace(value)) return;
+ var parts = value.Split(',');
+ ShowJoins = parts.Contains("join");
+ ShowLeaves = parts.Contains("leave");
+ ShowKnocks = parts.Contains("knock");
+ ShowInvites = parts.Contains("invite");
+ ShowBans = parts.Contains("ban");
StateHasChanged();
}
- }
-
- private bool _showLeaves = true;
+ } = "";
- private bool ShowLeaves {
- get => _showLeaves;
- set {
- _showLeaves = value;
- StateHasChanged();
- }
- }
+#endregion
- private bool _showUpdates = true;
+ private ObservableCollection<string> Log { get; set; } = new();
+ private List<StateEventResponse> Memberships { get; set; } = [];
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; }
- private bool ShowUpdates {
- get => _showUpdates;
- set {
- _showUpdates = value;
- StateHasChanged();
- }
- }
+ [Parameter, SupplyParameterFromQuery(Name = "room")]
+ public string RoomId { get; set; } = "";
- private bool _showKnocks = true;
+ protected override async Task OnInitializedAsync() {
+ Log.CollectionChanged += (sender, args) => StateHasChanged();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (Homeserver is null) return;
- private bool ShowKnocks {
- get => _showKnocks;
- set {
- _showKnocks = value;
- StateHasChanged();
- }
+ StateHasChanged();
+ Console.WriteLine("Rerendered!");
+ await base.OnInitializedAsync();
+ if (!string.IsNullOrWhiteSpace(RoomId))
+ await Execute();
}
- private bool _showInvites = true;
+ private async Task Execute() {
+ Memberships.Clear();
+ var room = Homeserver.GetRoom(RoomId);
+ var filter = new SyncFilter.EventFilter() { Types = [RoomMemberEventContent.EventId] };
+ var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(ignoreNull: true, indent: false));
+ await foreach (var resp in events) {
+ var all = resp.State.Concat(resp.Chunk)
+ // ugly hack, because some users fuck around too much
+ .Select(x => {
+ if (x.RawContent?["displayname"]?.GetValueKind() != JsonValueKind.String)
+ x.RawContent?.Remove("displayname");
+ if (x.RawContent?["avatar_url"]?.GetValueKind() is not JsonValueKind.String)
+ x.RawContent?.Remove("avatar_url");
+ return x;
+ });
+ Memberships.AddRange(all.Where(x => x.Type == RoomMemberEventContent.EventId));
- private bool ShowInvites {
- get => _showInvites;
- set {
- _showInvites = value;
- StateHasChanged();
+ Log.Add($"Got {resp.State.Count} state and {resp.Chunk.Count} timeline events.");
}
- }
- private bool _showKicks = true;
+ Log.Add("Reached end of timeline!");
- private bool ShowKicks {
- get => _showKicks;
- set {
- _showKicks = value;
- StateHasChanged();
- }
+ StateHasChanged();
}
- private bool _showBans = true;
+ private readonly struct MembershipEntry {
+ public required MembershipTransition State { get; init; }
+ public required StateEventResponse Event { get; init; }
+ public required StateEventResponse? Previous { get; init; }
- private bool ShowBans {
- get => _showBans;
- set {
- _showBans = value;
- StateHasChanged();
+ public void Deconstruct(out MembershipTransition transition, out StateEventResponse evt, out StateEventResponse? prev) {
+ transition = State;
+ evt = Event;
+ prev = Previous;
}
}
-
- private string sender = "";
-
- private string Sender {
- get => sender;
- set {
- sender = value;
- StateHasChanged();
- }
+
+ private enum MembershipTransition : byte {
+ None,
+ Join,
+ Leave,
+ Knock,
+ Invite,
+ Ban,
+
+ // disambiguated
+ ProfileUpdate,
+ Kick,
+ Unban,
+ InviteAccepted,
+ InviteRejected,
+ InviteRetracted,
+ KnockAccepted,
+ KnockRejected,
+ KnockRetracted
}
-
- private string user = "";
-
- private string User {
- get => user;
- set {
- user = value;
- StateHasChanged();
+
+ private static IEnumerable<MembershipEntry> GetTransitions(List<StateEventResponse> evts) {
+ Dictionary<string, MembershipEntry> transitions = new();
+ foreach (var evt in evts.OrderBy(x => x.OriginServerTs)) {
+ var content = evt.TypedContent as RoomMemberEventContent ?? throw new InvalidOperationException("Event is not a RoomMemberEventContent!");
+ var prev = transitions.GetValueOrDefault(evt.StateKey!) as MembershipEntry?;
+ transitions[evt.StateKey ?? throw new Exception("Member event has no state key??")] = new MembershipEntry {
+ Event = evt,
+ Previous = prev?.Event,
+ State = content.Membership switch {
+ RoomMemberEventContent.MembershipTypes.Join =>
+ prev?.State switch {
+ MembershipTransition.Join or MembershipTransition.InviteAccepted => MembershipTransition.ProfileUpdate,
+ MembershipTransition.Invite => MembershipTransition.InviteAccepted,
+ _ => MembershipTransition.Join
+ },
+ RoomMemberEventContent.MembershipTypes.Leave =>
+ evt.Sender == evt.StateKey
+ ? prev?.State switch {
+ MembershipTransition.Knock => MembershipTransition.KnockRetracted,
+ MembershipTransition.Invite => MembershipTransition.InviteRejected,
+ _ => MembershipTransition.Leave
+ }
+ : prev?.State switch {
+ // not self
+ MembershipTransition.Knock => MembershipTransition.KnockRejected,
+ MembershipTransition.Invite => MembershipTransition.InviteRetracted,
+ _ => MembershipTransition.Kick,
+ },
+ RoomMemberEventContent.MembershipTypes.Invite =>
+ prev?.State switch {
+ MembershipTransition.Knock => MembershipTransition.KnockAccepted,
+ _ => MembershipTransition.Invite
+ },
+ RoomMemberEventContent.MembershipTypes.Knock => MembershipTransition.Knock,
+ RoomMemberEventContent.MembershipTypes.Ban => MembershipTransition.Ban,
+ _ => MembershipTransition.None
+ }
+ };
+ yield return transitions[evt.StateKey];
}
}
-#endregion
-
- private ObservableCollection<string> log { get; set; } = new();
- private List<StateEventResponse> Memberships { get; set; } = [];
- private AuthenticatedHomeserverGeneric hs { get; set; }
-
- [Parameter, SupplyParameterFromQuery(Name = "room")]
- public string roomId { get; set; }
-
- protected override async Task OnInitializedAsync() {
- log.CollectionChanged += (sender, args) => StateHasChanged();
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
- if (hs is null) return;
+ private IEnumerable<MembershipEntry> Disambiguated(IEnumerable<MembershipEntry> entries) {
+ FrozenDictionary<MembershipTransition, MembershipTransition> disambiguated = new Dictionary<MembershipTransition, MembershipTransition>() {
+ { MembershipTransition.ProfileUpdate, MembershipTransition.Join },
+ { MembershipTransition.Kick, MembershipTransition.Leave },
+ { MembershipTransition.Unban, MembershipTransition.Leave },
+ { MembershipTransition.InviteAccepted, MembershipTransition.Join },
+ { MembershipTransition.InviteRejected, MembershipTransition.Leave },
+ { MembershipTransition.InviteRetracted, MembershipTransition.Leave },
+ { MembershipTransition.KnockAccepted, MembershipTransition.Invite },
+ { MembershipTransition.KnockRejected, MembershipTransition.Leave },
+ { MembershipTransition.KnockRetracted, MembershipTransition.Leave }
+ }.ToFrozenDictionary();
+
+ foreach (var entry in entries) {
+ if (!DoDisambiguate) {
+ yield return entry;
+ continue;
+ }
- StateHasChanged();
- Console.WriteLine("Rerendered!");
- await base.OnInitializedAsync();
- if (!string.IsNullOrWhiteSpace(roomId))
- await Execute();
+ var newState = entry.State switch {
+ MembershipTransition.ProfileUpdate when !DoDisambiguate || !DisambiguateProfileUpdates => MembershipTransition.Join,
+ MembershipTransition.Kick when !DoDisambiguate || !DisambiguateKicks => MembershipTransition.Leave,
+ MembershipTransition.Unban when !DoDisambiguate || !DisambiguateUnbans => MembershipTransition.Leave,
+ MembershipTransition.InviteAccepted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted => MembershipTransition.Join,
+ MembershipTransition.InviteRejected when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected => MembershipTransition.Leave,
+ MembershipTransition.InviteRetracted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted => MembershipTransition.Leave,
+ MembershipTransition.KnockAccepted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted => MembershipTransition.Invite,
+ MembershipTransition.KnockRejected when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected => MembershipTransition.Leave,
+ MembershipTransition.KnockRetracted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted => MembershipTransition.Leave,
+ _ => entry.State
+ };
+ if (newState != entry.State) {
+ yield return entry with { State = newState };
+ }
+ else yield return entry;
+ }
}
- private async Task Execute() {
- Memberships.Clear();
- var room = hs.GetRoom(roomId);
- var events = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 5000);
- await foreach (var resp in events) {
- var all = resp.State.Concat(resp.Chunk);
- Memberships.AddRange(all.Where(x => x.Type == RoomMemberEventContent.EventId));
-
- log.Add($"{resp.State.Count} state, {resp.Chunk.Count} timeline");
+ private IEnumerable<MembershipEntry> GetFilteredMemberships() {
+ var filteredMemberships = GetTransitions(Memberships);
+ if (!string.IsNullOrWhiteSpace(Sender)) filteredMemberships = filteredMemberships.Where(x => x.Event.Sender == Sender);
+ if (!string.IsNullOrWhiteSpace(User)) filteredMemberships = filteredMemberships.Where(x => x.Event.StateKey == User);
+ filteredMemberships = Disambiguated(filteredMemberships);
+
+ if (!ShowJoins) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Join);
+ if (!ShowLeaves) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Leave);
+ if (!ShowKnocks) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Knock);
+ if (!ShowInvites) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Invite);
+ if (!ShowBans) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Ban);
+ // extended filters
+ if (DoDisambiguate) {
+ if (!DisambiguateProfileUpdates || !ShowProfileUpdates) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.ProfileUpdate);
+ if (!DisambiguateKicks || !ShowKicks) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Kick);
+ if (!DisambiguateUnbans || !ShowUnbans) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Unban);
+ if (!DisambiguateInviteActions || !ShowInviteActions || !DisambiguateInviteAccepted || !ShowInviteAccepted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.InviteAccepted);
+ if (!DisambiguateInviteActions || !ShowInviteActions || !DisambiguateInviteRejected || !ShowInviteRejected) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.InviteRejected);
+ if (!DisambiguateInviteActions || !ShowInviteActions || !DisambiguateInviteRetracted || !ShowInviteRetracted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.InviteRetracted);
+ if (!DisambiguateKnockActions || !ShowKnockActions || !DisambiguateKnockAccepted || !ShowKnockAccepted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.KnockAccepted);
+ if (!DisambiguateKnockActions || !ShowKnockActions || !DisambiguateKnockRejected || !ShowKnockRejected) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.KnockRejected);
+ if (!DisambiguateKnockActions || !ShowKnockActions || !DisambiguateKnockRetracted || !ShowKnockRetracted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.KnockRetracted);
}
- StateHasChanged();
+ if (!ChronologicalOrder) filteredMemberships = filteredMemberships.Reverse();
+
+ return filteredMemberships;
}
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
index b8baeb8..736e59a 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
@@ -2,7 +2,7 @@
@using LibMatrix.RoomTypes
@using System.Collections.ObjectModel
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
<h3>Room intersections</h3>
<hr/>
@@ -113,7 +113,7 @@
protected override async Task OnInitializedAsync() {
Log.CollectionChanged += (sender, args) => StateHasChanged();
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
StateHasChanged();
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
index 915f8dc..c3cc09c 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
@@ -3,13 +3,15 @@
@using LibMatrix.RoomTypes
@using System.Collections.ObjectModel
@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
<h3>User Trace</h3>
<hr/>
<p>Users: </p>
<InputTextArea @bind-Value="@UserIdString"></InputTextArea>
<br/>
-<InputText @bind-Value="@ImportFromRoomId"></InputText><LinkButton OnClick="@DoImportFromRoomId">Import from room (ID)</LinkButton>
+<InputText @bind-Value="@ImportFromRoomId"></InputText>
+<LinkButton OnClick="@DoImportFromRoomId">Import from room (ID)</LinkButton>
<details>
<summary>Rooms to be searched (@rooms.Count)</summary>
@@ -24,18 +26,21 @@
<details>
<summary>Results</summary>
- @foreach (var (userId, events) in matches) {
+ @foreach (var (userId, events) in matches.OrderBy(x=>x.Key)) {
<h4>@userId</h4>
- <ul>
- @foreach (var match in events) {
- <li>
- <ul>
- <li>@match.RoomName (<span>@match.Room.RoomId</span>)</li>
- <li>Membership: @(match.Event.RawContent.ToJson(indent: false))</li>
- </ul>
- </li>
+ <table>
+ @foreach (var match in events.OrderBy(x=>x.RoomName)) {
+ <tr>
+ <td>@match.RoomName (<span>@match.Room.RoomId</span>)</td>
+ <td>
+ <details>
+ <summary>@SummarizeMembership(match.Event)</summary>
+ <pre>@match.Event.RawContent.ToJson(indent: true)</pre>
+ </details>
+ </td>
+ </tr>
}
- </ul>
+ </table>
}
</details>
@@ -61,56 +66,34 @@
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
- // var sessions = await RMUStorage.GetAllTokens();
- // var baseRooms = new List<GenericRoom>();
- // foreach (var userAuth in sessions) {
- // var session = await RMUStorage.GetSession(userAuth);
- // if (session is not null) {
- // baseRooms.AddRange(await session.GetJoinedRooms());
- // var sessionRooms = (await session.GetJoinedRooms()).Where(x => !rooms.Any(y => y.Room.RoomId == x.RoomId)).ToList();
- // StateHasChanged();
- // log.Add($"Got {sessionRooms.Count} rooms for {userAuth.UserId}");
- // }
- // }
- //
- // log.Add("Done fetching rooms!");
- //
- // baseRooms = baseRooms.DistinctBy(x => x.RoomId).ToList();
- //
- // // rooms.CollectionChanged += (sender, args) => StateHasChanged();
- // var tasks = baseRooms.Select(async newRoom => {
- // bool success = false;
- // while (!success)
- // try {
- // var state = await newRoom.GetFullStateAsListAsync();
- // var newRoomInfo = new RoomInfo(newRoom, state);
- // rooms.Add(newRoomInfo);
- // log.Add($"Got {newRoomInfo.StateEvents.Count} events for {newRoomInfo.RoomName}");
- // success = true;
- // }
- // catch (MatrixException e) {
- // log.Add($"Failed to fetch room {newRoom.RoomId}! {e}");
- // throw;
- // }
- // catch (HttpRequestException e) {
- // log.Add($"Failed to fetch room {newRoom.RoomId}! {e}");
- // }
- // });
- // await Task.WhenAll(tasks);
- //
- // log.Add($"Done fetching members!");
- //
- // UserIDs.RemoveAll(x => sessions.Any(y => y.UserId == x));
-
- foreach (var session in await RMUStorage.GetAllTokens()) {
- var _hs = await RMUStorage.GetSession(session);
- if (_hs is not null) {
- rooms.AddRange(await _hs.GetJoinedRooms());
- log.Add($"Got {rooms.Count} rooms after adding {_hs.UserId}");
+
+ var sessions = await sessionStore.GetAllSessions();
+ var tasks = sessions.Select(async session => {
+ try {
+ var _hs = await sessionStore.GetHomeserver(session.Key);
+ if (_hs is not null) {
+ try {
+ var _rooms = await _hs.GetJoinedRooms();
+ if (!_rooms.Any()) return;
+ // Check if homeserver supports `?format=event`:
+ await _rooms.First().GetStateEventAsync(RoomMemberEventContent.EventId, session.Value.Auth.UserId);
+ rooms.AddRange(_rooms);
+ log.Add($"Got {_rooms.Count} rooms for {_hs.UserId}, total {rooms.Count}");
+ }
+ catch (Exception e) {
+ if (e is LibMatrixException { ErrorCode: LibMatrixException.ErrorCodes.M_UNSUPPORTED })
+ log.Add($"Homeserver {_hs.UserId} does not support `?format=event`! Skipping...");
+ else log.Add($"Failed to fetch rooms for {_hs.UserId}! {e}");
+ }
+ }
}
- }
+ catch (Exception e) {
+ log.Add($"Failed to fetch rooms for {session.Value.Auth.UserId}! {e}");
+ }
+ });
+ await Task.WhenAll(tasks);
//get distinct rooms evenly distributed per session, accounting for count per session
rooms = rooms.OrderBy(x => rooms.Count(y => y.Homeserver == x.Homeserver)).DistinctBy(x => x.RoomId).ToList();
@@ -125,17 +108,6 @@
foreach (var userId in UserIDs) {
matches.Add(userId, new List<Match>());
- // foreach (var room in rooms) {
- // var state = room.StateEvents.Where(x => x!.Type == RoomMemberEventContent.EventId).ToList();
- // if (state!.Any(x => x.StateKey == userId)) {
- // matches[userId].Add(new() {
- // Event = state.First(x => x.StateKey == userId),
- // Room = room.Room,
- // RoomName = room.RoomName ?? "No name"
- // });
- // }
- // }
-
log.Add($"Searching for {userId}...");
await foreach (var match in GetMatches(userId)) {
matches[userId].Add(match);
@@ -173,13 +145,19 @@
private async IAsyncEnumerable<Match> GetMatches(string userId) {
var results = rooms.Select(async room => {
- var state = await room.GetStateEventOrNullAsync(room.RoomId, userId);
- if (state is not null) {
- return new Match {
- Room = room,
- Event = state,
- RoomName = await room.GetNameOrFallbackAsync()
- };
+ try {
+ var state = await room.GetStateEventOrNullAsync(RoomMemberEventContent.EventId, userId);
+ if (state is not null) {
+ log.Add($"Found {userId} in {room.RoomId} with membership {state.RawContent.ToJson(indent: false)}");
+ return new Match {
+ Room = room,
+ Event = state,
+ RoomName = await room.GetNameOrFallbackAsync()
+ };
+ }
+ }
+ catch (Exception e) {
+ log.Add($"Failed to fetch state for {userId} in {room.RoomId}! {e}");
}
return null;
@@ -191,4 +169,27 @@
}
}
+ public string SummarizeMembership(StateEventResponse state) {
+ var membership = state.ContentAs<RoomMemberEventContent>();
+ var time = DateTimeOffset.FromUnixTimeMilliseconds(state.OriginServerTs!.Value);
+ return membership switch {
+ { Membership: "invite", Reason: null } => $"Invited by {state.Sender} at {time}",
+ { Membership: "invite", Reason: not null } => $"Invited by {state.Sender} at {time} for {membership.Reason}",
+ { Membership: "join", Reason: null } => $"Joined at {time}",
+ { Membership: "join", Reason: not null } => $"Joined at {time} for {membership.Reason}",
+ { Membership: "leave", Reason: null } => state.Sender == state.StateKey ? $"Left at {time}" : $"Kicked by {state.Sender} at {time}",
+ { Membership: "leave", Reason: not null } => state.Sender == state.StateKey ? $"Left at {time} with reason {membership.Reason}" : $"Kicked by {state.Sender} at {time} for {membership.Reason}",
+ { Membership: "ban", Reason: null } => $"Banned by {state.Sender} at {time}",
+ { Membership: "ban", Reason: not null } => $"Banned by {state.Sender} at {time} for {membership.Reason}",
+ { Membership: "knock", Reason: null } => $"Knocked at {time}",
+ { Membership: "knock", Reason: not null } => $"Knocked at {time} for {membership.Reason}",
+ _ => $"Unknown membership {membership.Membership}, sent at {time} by {state.Sender} for {membership.Reason}"
+ };
+ }
+
+ private async Task ExportJson() {
+ var json = matches.ToJson();
+
+ }
+
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor b/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
index 80a03f2..ac3c651 100644
--- a/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
+++ b/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
@@ -1,6 +1,6 @@
@page "/Tools/Room/SpaceRestrictedJoins"
@using System.Collections.ObjectModel
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
<h3>Allow space to restricted join children</h3>
<hr/>
@@ -31,7 +31,7 @@
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
StateHasChanged();
diff --git a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
index 667b518..e5ffd5b 100644
--- a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
@@ -1,7 +1,7 @@
@page "/Tools/CopyPowerlevel"
@using ArcaneLibs.Extensions
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.RoomTypes
<h3>Copy powerlevel</h3>
<hr/>
@@ -23,11 +23,11 @@
List<AuthenticatedHomeserverGeneric> hss { get; set; } = new();
protected override async Task OnInitializedAsync() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
- var sessions = await RMUStorage.GetAllTokens();
- foreach (var userAuth in sessions) {
- var session = await RMUStorage.GetSession(userAuth);
+ var sessions = await sessionStore.GetAllSessions();
+ foreach (var userAuth in sessions.Keys) {
+ var session = await sessionStore.GetHomeserver(userAuth);
if (session is not null) {
hss.Add(session);
StateHasChanged();
@@ -42,7 +42,7 @@
private async Task Execute() {
foreach (var hs in hss) {
var rooms = await hs.GetJoinedRooms();
- var tasks = rooms.Select(x=>Execute(hs, x)).ToAsyncEnumerable();
+ var tasks = rooms.Select(x=>ApplyPowerlevelsInRoom(hs, x)).ToAsyncEnumerable();
await foreach (var a in tasks) {
if (!string.IsNullOrWhiteSpace(a)) {
log.Add(a);
@@ -52,7 +52,7 @@
}
}
- private async Task<string> Execute(AuthenticatedHomeserverGeneric hs, GenericRoom room) {
+ private async Task<string> ApplyPowerlevelsInRoom(AuthenticatedHomeserverGeneric hs, GenericRoom room) {
try {
var pls = await room.GetPowerLevelsAsync();
// if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) == pls.UsersDefault) return "I am default PL in " + room.RoomId;
diff --git a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
index a2ad388..c373a37 100644
--- a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
@@ -1,7 +1,7 @@
@page "/Tools/MassRoomJoin"
@using ArcaneLibs.Extensions
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
<h3>Mass join room</h3>
<hr/>
<p>Room: </p>
@@ -25,11 +25,11 @@
string roomId { get; set; }
protected override async Task OnInitializedAsync() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
- var sessions = await RMUStorage.GetAllTokens();
- foreach (var userAuth in sessions) {
- var session = await RMUStorage.GetSession(userAuth);
+ var sessions = await sessionStore.GetAllSessions();
+ foreach (var userAuth in sessions.Keys) {
+ var session = await sessionStore.GetHomeserver(userAuth);
if (session is not null) {
hss.Add(session);
StateHasChanged();
diff --git a/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor b/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor
index d8b02bb..a393d2e 100644
--- a/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor
@@ -1,4 +1,4 @@
-@page "/Tools/ViewAccountData"
+@page "/Tools/User/ViewAccountData"
@using ArcaneLibs.Extensions
@using LibMatrix
<h3>View account data</h3>
@@ -16,7 +16,7 @@
Dictionary<string, EventList?> perRoomAccountData = new();
protected override async Task OnInitializedAsync() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
perRoomAccountData = await hs.EnumerateAccountDataPerRoom();
globalAccountData = await hs.EnumerateAccountData();
diff --git a/MatrixUtils.Web/Pages/User/DMManager.razor b/MatrixUtils.Web/Pages/User/DMManager.razor
index 80bf3b2..4b8b7c2 100644
--- a/MatrixUtils.Web/Pages/User/DMManager.razor
+++ b/MatrixUtils.Web/Pages/User/DMManager.razor
@@ -1,8 +1,8 @@
@page "/User/DirectMessages"
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.Responses
@using MatrixUtils.Abstractions
@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
<h3>Direct Messages</h3>
<hr/>
@@ -29,7 +29,7 @@
}
protected override async Task OnInitializedAsync() {
- Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
Status = "Loading global profile...";
if (Homeserver.WhoAmI?.UserId is null) return;
diff --git a/MatrixUtils.Web/Pages/User/Profile.razor b/MatrixUtils.Web/Pages/User/Profile.razor
index 49af22f..ccd3e7b 100644
--- a/MatrixUtils.Web/Pages/User/Profile.razor
+++ b/MatrixUtils.Web/Pages/User/Profile.razor
@@ -1,10 +1,8 @@
@page "/User/Profile"
-@using LibMatrix.EventTypes.Spec.State
-@using ArcaneLibs.Extensions
@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.Responses
@using MatrixUtils.Abstractions
-@using Microsoft.AspNetCore.Components.Forms
<h3>Manage Profile - @Homeserver?.WhoAmI?.UserId</h3>
<hr/>
@@ -12,7 +10,7 @@
<h4>Profile</h4>
<hr/>
<div>
- <img src="@Homeserver.ResolveMediaUri(NewProfile.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/>
+ <MxcAvatar Homeserver="@Homeserver" MxcUri="@NewProfile.AvatarUrl" Circular="true" Size="96"/>
<div style="display: inline-block; vertical-align: middle;">
<span>Display name: </span><FancyTextBox @bind-Value="@NewProfile.DisplayName"></FancyTextBox><br/>
<span>Avatar URL: </span><FancyTextBox @bind-Value="@NewProfile.AvatarUrl"></FancyTextBox>
@@ -35,12 +33,13 @@
<summary style="@(room.OwnMembership?.DisplayName == OldProfile.DisplayName && room.OwnMembership?.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")">
<div style="display: inline-block; width: calc(100% - 50px); vertical-align: middle; margin-top: -8px; margin-bottom: -8px;">
<CascadingValue Value="OldProfile">
- <RoomListItem ShowOwnProfile="true" RoomInfo="@room" OwnMemberState="@room.OwnMembership"></RoomListItem>
+ <RoomListItem Homeserver="Homeserver" ShowOwnProfile="true" RoomInfo="@room" OwnMemberState="@room.OwnMembership"></RoomListItem>
</CascadingValue>
</div>
</summary>
@if (room.OwnMembership is not null) {
- <img src="@Homeserver.ResolveMediaUri(room.OwnMembership.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/>
+ @* <img src="@Homeserver.ResolveMediaUri(room.OwnMembership.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/> *@
+ <MxcAvatar Homeserver="@Homeserver" MxcUri="@room.OwnMembership.AvatarUrl" Circular="true" Size="96"/>
<div style="display: inline-block; vertical-align: middle;">
<span>Display name: </span><FancyTextBox BackgroundColor="@(room.OwnMembership.DisplayName == OldProfile.DisplayName ? "" : "#ffff0033")" @bind-Value="@room.OwnMembership.DisplayName"></FancyTextBox><br/>
<span>Avatar URL: </span><FancyTextBox BackgroundColor="@(room.OwnMembership.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")" @bind-Value="@room.OwnMembership.AvatarUrl"></FancyTextBox>
@@ -58,29 +57,11 @@
</details>
<br/>
}
-
- @foreach (var (roomId, roomProfile) in RoomProfiles.OrderBy(x => RoomNames.TryGetValue(x.Key, out var _name) ? _name : x.Key)) {
- <details class="details-compact">
- <summary style="@(roomProfile.DisplayName == OldProfile.DisplayName && roomProfile.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")">@(RoomNames.TryGetValue(roomId, out var name) ? name : roomId)</summary>
- <img src="@Homeserver.ResolveMediaUri(roomProfile.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/>
- <div style="display: inline-block; vertical-align: middle;">
- <span>Display name: </span><FancyTextBox BackgroundColor="@(roomProfile.DisplayName == OldProfile.DisplayName ? "" : "#ffff0033")" @bind-Value="@roomProfile.DisplayName"></FancyTextBox><br/>
- <span>Avatar URL: </span><FancyTextBox BackgroundColor="@(roomProfile.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")" @bind-Value="@roomProfile.AvatarUrl"></FancyTextBox>
- <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, roomId))"></InputFile><br/>
- <LinkButton OnClick="@(() => UpdateRoomProfile(roomId))">Update profile</LinkButton>
- </div>
- <br/>
- @if (!string.IsNullOrWhiteSpace(Status)) {
- <p>@Status</p>
- }
- </details>
- <br/>
- }
// </details>
}
@code {
- private string? _status = null;
+ private string? _status;
private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
private UserProfileResponse? NewProfile { get; set; }
@@ -99,7 +80,7 @@
private Dictionary<string, string> RoomNames { get; set; } = new();
protected override async Task OnInitializedAsync() {
- Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
Status = "Loading global profile...";
if (Homeserver.WhoAmI?.UserId is null) return;
@@ -107,44 +88,50 @@
OldProfile = (await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId)); //.DeepClone();
Status = "Loading room profiles...";
var roomProfiles = Homeserver.GetRoomProfilesAsync();
+ List<Task> roomInfoTasks = [];
await foreach (var (roomId, roomProfile) in roomProfiles) {
- var room = Homeserver.GetRoom(roomId);
- var roomNameTask = room.GetNameOrFallbackAsync();
- var roomIconTask = room.GetAvatarUrlAsync();
- var roomInfo = new RoomInfo(room) {
- OwnMembership = roomProfile
- };
- try {
- roomInfo.RoomIcon = (await roomIconTask).Url;
- }
- catch (MatrixException e) {
- if (e is not { ErrorCode: "M_NOT_FOUND" }) throw;
- }
+ var task = Task.Run(async () => {
+ var room = Homeserver.GetRoom(roomId);
+ var roomNameTask = room.GetNameOrFallbackAsync();
+ var roomIconTask = room.GetAvatarUrlAsync();
+ var roomInfo = new RoomInfo(room) {
+ OwnMembership = roomProfile
+ };
+ try {
+ roomInfo.RoomIcon = (await roomIconTask).Url;
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_NOT_FOUND" }) throw;
+ }
- try {
- roomInfo.RoomName = await roomNameTask;
- }
- catch (MatrixException e) {
- if (e is not { ErrorCode: "M_NOT_FOUND" }) throw;
- }
+ try {
+ RoomNames[roomId] = roomInfo.RoomName = await roomNameTask;
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_NOT_FOUND" }) throw;
+ }
- Rooms.Add(roomInfo);
- // Status = $"Got profile for {roomId}...";
- RoomProfiles[roomId] = roomProfile; //.DeepClone();
+ Rooms.Add(roomInfo);
+ // Status = $"Got profile for {roomId}...";
+ RoomProfiles[roomId] = roomProfile; //.DeepClone();
+ });
+ roomInfoTasks.Add(task);
}
+
+ await Task.WhenAll(roomInfoTasks);
StateHasChanged();
Status = "Room profiles loaded, loading room names...";
- var roomNameTasks = RoomProfiles.Keys.Select(x => Homeserver.GetRoom(x)).Select(async x => {
- var name = await x.GetNameOrFallbackAsync();
- return new KeyValuePair<string, string?>(x.RoomId, name);
- }).ToAsyncEnumerable();
+ // var roomNameTasks = RoomProfiles.Keys.Select(x => Homeserver.GetRoom(x)).Select(async x => {
+ // var name = await x.GetNameOrFallbackAsync();
+ // return new KeyValuePair<string, string?>(x.RoomId, name);
+ // }).ToAsyncEnumerable();
- await foreach (var (roomId, roomName) in roomNameTasks) {
- // Status = $"Got room name for {roomId}: {roomName}";
- RoomNames[roomId] = roomName;
- }
+ // await foreach (var (roomId, roomName) in roomNameTasks) {
+ // Status = $"Got room name for {roomId}: {roomName}";
+ // RoomNames[roomId] = roomName;
+ // }
StateHasChanged();
Status = null;
diff --git a/MatrixUtils.Web/Program.cs b/MatrixUtils.Web/Program.cs
index 1b8960c..14cf2fc 100644
--- a/MatrixUtils.Web/Program.cs
+++ b/MatrixUtils.Web/Program.cs
@@ -8,6 +8,8 @@ using MatrixUtils.Web;
using MatrixUtils.Web.Classes;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using SpawnDev.BlazorJS;
+using SpawnDev.BlazorJS.WebWorkers;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
@@ -16,6 +18,18 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
// builder.Logging.SetMinimumLevel(LogLevel.Trace);
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
+builder.Services.AddBlazorJSRuntime();
+builder.Services.AddWebWorkerService(webWorkerService =>
+{
+ // Optionally configure the WebWorkerService service before it is used
+ // Default WebWorkerService.TaskPool settings: PoolSize = 0, MaxPoolSize = 1, AutoGrow = true
+ // Below sets TaskPool max size to 2. By default the TaskPool size will grow as needed up to the max pool size.
+ // Setting max pool size to -1 will set it to the value of navigator.hardwareConcurrency
+ webWorkerService.TaskPool.MaxPoolSize = 2;
+ // Below is telling the WebWorkerService TaskPool to set the initial size to 2 if running in a Window scope and 0 otherwise
+ // This starts up 2 WebWorkers to handle TaskPool tasks as needed
+ webWorkerService.TaskPool.PoolSize = webWorkerService.GlobalScope == GlobalScope.Window ? 0 : 0;
+});
try {
builder.Configuration.AddJsonStream(await new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }.GetStreamAsync("/appsettings.json"));
@@ -64,5 +78,6 @@ builder.Services.AddScoped<TieredStorageService>(x =>
);
builder.Services.AddRoryLibMatrixServices();
-builder.Services.AddScoped<RMUStorageWrapper>();
-await builder.Build().RunAsync();
\ No newline at end of file
+builder.Services.AddScoped<RmuSessionStore>();
+// await builder.Build().RunAsync();
+await builder.Build().BlazorJSRunAsync();
\ No newline at end of file
diff --git a/MatrixUtils.Web/Properties/launchSettings.json b/MatrixUtils.Web/Properties/launchSettings.json
index aa41dc8..660211d 100644
--- a/MatrixUtils.Web/Properties/launchSettings.json
+++ b/MatrixUtils.Web/Properties/launchSettings.json
@@ -13,7 +13,7 @@
"dotnetRunMessages": true,
"launchBrowser": false,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
- "applicationUrl": "http://localhost:5117",
+ "applicationUrl": "http://*:5117",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/MatrixUtils.Web/Shared/InlineUserItem.razor b/MatrixUtils.Web/Shared/InlineUserItem.razor
index 9c6608a..eaf7a92 100644
--- a/MatrixUtils.Web/Shared/InlineUserItem.razor
+++ b/MatrixUtils.Web/Shared/InlineUserItem.razor
@@ -1,4 +1,4 @@
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.Responses
<div style="background-color: #ffffff11; border-radius: 0.5em; height: 1em; display: inline-block; vertical-align: middle;" alt="@UserId">
<img style="@(ChildContent is not null ? "vertical-align: baseline;" : "vertical-align: top;") width: 1em; height: 1em; border-radius: 50%;" src="@ProfileAvatar"/>
@@ -39,7 +39,6 @@
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
- Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate();
if(Homeserver is null) return;
await _semaphoreSlim.WaitAsync();
@@ -59,7 +58,7 @@
}
- ProfileAvatar ??= Homeserver.ResolveMediaUri(User.AvatarUrl);
+ // ProfileAvatar ??= Homeserver.ResolveMediaUri(User.AvatarUrl);
ProfileName ??= User.DisplayName;
_semaphoreSlim.Release();
diff --git a/MatrixUtils.Web/Shared/MainLayout.razor b/MatrixUtils.Web/Shared/MainLayout.razor
index c67f73c..b32735f 100644
--- a/MatrixUtils.Web/Shared/MainLayout.razor
+++ b/MatrixUtils.Web/Shared/MainLayout.razor
@@ -1,5 +1,4 @@
-@using ArcaneLibs
-@inherits LayoutComponentBase
+@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
@@ -10,16 +9,15 @@
<div class="top-row px-4">
@* <PortableDevTools/> *@
@* <ResourceUsage/> *@
- <a style="color: #ccc; text-decoration: underline" href="https://cgit.rory.gay/matrix/MatrixRoomUtils.git/" target="_blank">Git</a>
- <a style="color: #ccc; text-decoration: underline" href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support" target="_blank">Matrix</a>
+ <a style="color: #ccc; text-decoration: underline" href="https://cgit.rory.gay/matrix/tools/MatrixUtils.git/" target="_blank">Git</a>
+ <a style="color: #ccc; text-decoration: underline" href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support"
+ target="_blank">Matrix</a>
</div>
<article class="Content px-4">
@Body
</article>
-
-
</main>
</div>
-<UpdateAvailableDetector/>
\ No newline at end of file
+<UpdateAvailableDetector/>
diff --git a/MatrixUtils.Web/Shared/MainLayout.razor.css b/MatrixUtils.Web/Shared/MainLayout.razor.css
index 01a5066..924393b 100644
--- a/MatrixUtils.Web/Shared/MainLayout.razor.css
+++ b/MatrixUtils.Web/Shared/MainLayout.razor.css
@@ -57,6 +57,7 @@ main {
.sidebar {
width: 250px;
+ min-width: 250px;
height: 100vh;
position: sticky;
top: 0;
diff --git a/MatrixUtils.Web/Shared/MxcAvatar.razor b/MatrixUtils.Web/Shared/MxcAvatar.razor
new file mode 100644
index 0000000..822894a
--- /dev/null
+++ b/MatrixUtils.Web/Shared/MxcAvatar.razor
@@ -0,0 +1,49 @@
+<MxcImage Homeserver="@Homeserver" Uri="@MxcUri" style="@StyleString"/>
+
+@code {
+ private string _style;
+
+ [Parameter]
+ public string? MxcUri {
+ get;
+ set {
+ if(field == value) return;
+ field = value;
+ // UriHasChanged(value);
+ StateHasChanged();
+ }
+ }
+
+ [Parameter]
+ public bool Circular { get; set; }
+
+ [Parameter]
+ public int Size { get; set; } = 48;
+
+ [Parameter]
+ public string SizeUnit { get; set; } = "px";
+
+ [Parameter]
+ public required AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+ private string StyleString => $"{(Circular ? "border-radius: 50%;" : "")} width: {Size}{SizeUnit}; height: {Size}{SizeUnit}; object-fit: cover;";
+
+ private static readonly string Prefix = "mxc://";
+ private static readonly int PrefixLength = Prefix.Length;
+
+ // private async Task UriHasChanged(string? value) {
+ // if (string.IsNullOrWhiteSpace(value) || !value.StartsWith(Prefix)) {
+ // Console.WriteLine($"[MxcAvatar] UriHasChanged: {value} does not start with {Prefix}!");
+ // return;
+ // }
+ //
+ // if (Homeserver is null) {
+ // Console.WriteLine($"[MxcAvatar] Homeserver is required for MxcAvatar! URI: {MxcUri}, Homeserver: {Homeserver?.ToString() ?? "null"}");
+ // return;
+ // }
+ //
+ // Console.WriteLine($"[MxcAvatar] Homeserver: {Homeserver}");
+ // StateHasChanged();
+ // }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/MxcImage.razor b/MatrixUtils.Web/Shared/MxcImage.razor
index e651c3f..26609ee 100644
--- a/MatrixUtils.Web/Shared/MxcImage.razor
+++ b/MatrixUtils.Web/Shared/MxcImage.razor
@@ -1,69 +1,69 @@
-<img src="@ResolvedUri" style="@StyleString"/>
-@code {
- private string _mxcUri;
- private string _style;
- private string _resolvedUri;
+<AuthorizedImage src="@ResolvedUrl" AccessToken="@Homeserver?.AccessToken" style="@StyleString"/>
+@code {
[Parameter]
- public string MxcUri {
- get => _mxcUri ?? "";
+ public string? Uri {
+ get;
set {
- Console.WriteLine($"New MXC uri: {value}");
- _mxcUri = value;
+ // Console.WriteLine($"New MXC uri: {value}");
+ if (field == value) return;
+ field = value;
UriHasChanged(value);
}
}
+
[Parameter]
public bool Circular { get; set; }
-
+
[Parameter]
public int? Width { get; set; }
-
+
[Parameter]
public int? Height { get; set; }
-
+
[Parameter]
- public string Style {
- get => _style;
+ public string? Style {
+ get;
set {
- _style = value;
+ field = value;
StateHasChanged();
}
}
-
+
[Parameter]
- public RemoteHomeserver? Homeserver { get; set; }
+ public required AuthenticatedHomeserverGeneric Homeserver { get; set; }
- private string ResolvedUri {
- get => _resolvedUri;
+ private string? ResolvedUrl {
+ get;
set {
- _resolvedUri = value;
+ field = value;
StateHasChanged();
}
}
private string StyleString => $"{Style} {(Circular ? "border-radius: 50%;" : "")} {(Width.HasValue ? $"width: {Width}px;" : "")} {(Height.HasValue ? $"height: {Height}px;" : "")} object-fit: cover;";
-
- private static readonly string Prefix = "mxc://";
- private static readonly int PrefixLength = Prefix.Length;
- private async Task UriHasChanged(string value) {
- if (!value.StartsWith(Prefix)) {
- Console.WriteLine($"UriHasChanged: {value} does not start with {Prefix}, passing as resolved URI!!!");
- ResolvedUri = value;
- return;
- }
- var uri = value[PrefixLength..].Split('/');
- Console.WriteLine($"UriHasChanged: {value} {uri[0]}");
- if (Homeserver is null) {
- Console.WriteLine($"Homeserver is null, creating new remotehomeserver for {uri[0]}");
- Homeserver = await hsProvider.GetRemoteHomeserver(uri[0]);
+ // private static readonly string Prefix = "mxc://";
+ // private static readonly int PrefixLength = Prefix.Length;
+
+ private async Task UriHasChanged(string? value) {
+ try {
+ if (string.IsNullOrWhiteSpace(value)) {
+ ResolvedUrl = null;
+ return;
+ }
+
+ if (Homeserver is null) {
+ Console.WriteLine($"Homeserver is required for MxcImage! Uri: {value}, Homeserver: {Homeserver?.ToString() ?? "null"}");
+ return;
+ }
+
+ ResolvedUrl = await Homeserver.GetMediaUrlAsync(value);
+ // Console.WriteLine($"[MxcImage] Resolved URL: {ResolvedUrl}");
+ StateHasChanged();
+ } catch (Exception e) {
+ await Console.Error.WriteLineAsync($"Error resolving media URL: {e}");
}
- ResolvedUri = Homeserver.ResolveMediaUri(value);
- Console.WriteLine($"ResolvedUri: {ResolvedUri}");
}
- // [Parameter]
- // public string Class { get; set; }
-
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/NavMenu.razor b/MatrixUtils.Web/Shared/NavMenu.razor
index 770a246..7371e66 100644
--- a/MatrixUtils.Web/Shared/NavMenu.razor
+++ b/MatrixUtils.Web/Shared/NavMenu.razor
@@ -37,6 +37,12 @@
</div>
<div class="nav-item px-3">
+ <NavLink class="nav-link" href="PolicyLists">
+ <span class="oi oi-ban" aria-hidden="true"></span> Manage policy lists
+ </NavLink>
+ </div>
+
+ <div class="nav-item px-3">
<NavLink class="nav-link" href="User/Profile">
<span class="oi oi-person" aria-hidden="true"></span> Manage profile
</NavLink>
diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
new file mode 100644
index 0000000..11ba18a
--- /dev/null
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
@@ -0,0 +1,102 @@
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using System.Reflection
+@using ArcaneLibs.Attributes
+@using LibMatrix
+@using System.Collections.Frozen
+@using LibMatrix.EventTypes
+@using LibMatrix.RoomTypes
+<ModalWindow Title="@("Creating many new " + (PolicyTypes.ContainsKey(MappedType??"") ? PolicyTypes[MappedType!].GetFriendlyNamePluralOrNull()?.ToLower() ?? PolicyTypes[MappedType!].Name : "event"))"
+ OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="600">
+ <span>Policy type:</span>
+ <select @bind="@MappedType">
+ <option>Select a value</option>
+ @foreach (var (type, mappedType) in PolicyTypes) {
+ <option value="@type">@mappedType.GetFriendlyName().ToLower()</option>
+ }
+ </select><br/>
+
+ <span>Reason:</span>
+ <FancyTextBox @bind-Value="@Reason"></FancyTextBox><br/>
+
+ <span>Recommendation:</span>
+ <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox><br/>
+
+ <span>Entities:</span><br/>
+ <InputTextArea @bind-Value="@Users" style="width: 500px;"></InputTextArea><br/>
+
+
+ @* <details> *@
+ @* <summary>JSON data</summary> *@
+ @* <pre> *@
+ @* $1$ @PolicyEvent.ToJson(true, true) #1# *@
+ @* </pre> *@
+ @* </details> *@
+ <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton>
+ <LinkButton OnClick="@(() => { _ = Save(); return Task.CompletedTask; })"> Save </LinkButton>
+
+</ModalWindow>
+
+@code {
+
+ [Parameter]
+ public required Action OnClose { get; set; }
+
+ [Parameter]
+ public required Action OnSaved { get; set; }
+
+ [Parameter]
+ public required GenericRoom Room { get; set; }
+
+ public string Recommendation { get; set; } = "m.ban";
+ public string Reason { get; set; } = "spam";
+ public string Users { get; set; } = "";
+
+ private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+
+ private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
+ .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+
+ private string? MappedType { get; set; }
+
+ private async Task Save() {
+ try {
+ await DoActualSave();
+ }
+ catch (Exception e) {
+ Console.WriteLine($"Failed to save: {e}");
+ }
+ }
+
+ private async Task DoActualSave() {
+ Console.WriteLine($"Saving ---");
+ Console.WriteLine($"Users = {Users}");
+ var users = Users.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList();
+ var tasks = users.Select(x => ExecuteBan(Room, x)).ToList();
+ await Task.WhenAll(tasks);
+
+ OnSaved.Invoke();
+ }
+
+ private async Task ExecuteBan(GenericRoom room, string entity) {
+ bool success = false;
+ while (!success) {
+ try {
+ var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent;
+ content.Recommendation = Recommendation;
+ content.Reason = Reason;
+ content.Entity = entity;
+ await room.SendStateEventAsync(MappedType!, content.GetDraupnir2StateKey(), content);
+ success = true;
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) throw;
+ Console.WriteLine(e);
+ }
+ catch (Exception e) {
+ //ignored
+ Console.WriteLine(e);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
index 1bd00d1..5819bee 100644
--- a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
@@ -35,39 +35,61 @@
</thead>
<tbody>
@foreach (var prop in props) {
+ var isNullable = Nullable.GetUnderlyingType(prop.PropertyType) is not null;
<tr>
<td style="padding-right: 8px;">
<span>@prop.GetFriendlyName()</span>
- @if (Nullable.GetUnderlyingType(prop.PropertyType) is not null) {
+ @if (Nullable.GetUnderlyingType(prop.PropertyType) is null) {
<span style="color: red;">*</span>
}
</td>
@{
var getter = prop.GetGetMethod();
var setter = prop.GetSetMethod();
- }
- @switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) {
- case Type t when t == typeof(string):
- <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></FancyTextBox>
- break;
- default:
- <p style="color: red;">Unsupported type: @prop.PropertyType</p>
- break;
+ if (getter is null) {
+ <p style="color: red;">Missing property getter: @prop.Name</p>
+ }
+ else {
+ switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) {
+ case Type t when t == typeof(string):
+ <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@((string e) => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></FancyTextBox>
+ break;
+ case Type t when t == typeof(DateTime):
+ if (!isNullable) {
+ @* <InputDate TValue="DateTime" Value="@(getter?.Invoke(PolicyData, null) as DateTime? ?? new DateTime())" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></InputDate> *@
+ }
+ else {
+ var value = getter?.Invoke(PolicyData, null) as DateTime?;
+ if (value is null) {
+ <button @onclick="() => { setter?.Invoke(PolicyData, [DateTime.Now]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Add value</button>
+ }
+ else {
+ var notNullValue = Nullable.GetValueRefOrDefaultRef(ref value);
+ Console.WriteLine($"Value: {value?.ToString() ?? "null"}");
+ <InputDate TValue="DateTime" ValueExpression="@(() => notNullValue)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></InputDate>
+ <button @onclick="() => { setter?.Invoke(PolicyData, [null]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Remove value</button>
+ }
+ }
+
+ break;
+ default:
+ <p style="color: red;">Unsupported type: @prop.PropertyType</p>
+ break;
+ }
+ }
}
</tr>
}
</tbody>
</table>
- <br/>
- <pre>
- @PolicyEvent.ToJson(true, false)
- </pre>
+ <details>
+ <summary>JSON data</summary>
+ <pre>
+ @PolicyEvent.ToJson(true, true)
+ </pre>
+ </details>
<LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton>
<LinkButton OnClick="@(() => { OnSave.Invoke(PolicyEvent); return Task.CompletedTask; })"> Save </LinkButton>
- @* <span>Target entity: </span> *@
- @* <FancyTextBox @bind-Value="@policyData.Entity"></FancyTextBox><br/> *@
- @* <span>Reason: </span> *@
- @* <FancyTextBox @bind-Value="@policyData.Reason"></FancyTextBox> *@
}
else {
<p>Policy data is null</p>
@@ -102,7 +124,7 @@
.ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
private StateEventResponse? _policyEvent;
-
+
private string? MappedType {
get => _policyEvent?.Type;
set {
@@ -110,9 +132,9 @@
PolicyEvent.Type = value;
PolicyEvent.TypedContent ??= Activator.CreateInstance(PolicyTypes[value]) as PolicyRuleEventContent;
PolicyData = PolicyEvent.TypedContent as PolicyRuleEventContent;
+ PolicyData.Recommendation ??= "m.ban";
}
}
}
-
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/RoomList.razor b/MatrixUtils.Web/Shared/RoomList.razor
index 42c5a9f..ba9cd69 100644
--- a/MatrixUtils.Web/Shared/RoomList.razor
+++ b/MatrixUtils.Web/Shared/RoomList.razor
@@ -10,7 +10,7 @@
}
else {
@foreach (var category in RoomsWithTypes.OrderBy(x => x.Value.Count)) {
- <RoomListCategory Category="@category" GlobalProfile="@GlobalProfile"></RoomListCategory>
+ <RoomListCategory Category="@category" GlobalProfile="@GlobalProfile" Homeserver="@Homeserver"></RoomListCategory>
}
}
@@ -35,6 +35,9 @@ else {
}
[Parameter]
+ public AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+
+ [Parameter]
public UserProfileResponse? GlobalProfile { get; set; }
[Parameter]
diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
index 1f5ce89..1ab0a1a 100644
--- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
+++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
@@ -1,12 +1,12 @@
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using MatrixUtils.Web.Classes.Constants
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.Responses
@using MatrixUtils.Abstractions
<details open>
<summary>@RoomType (@Rooms.Count)</summary>
@foreach (var room in Rooms) {
<div class="room-list-item">
- <RoomListItem RoomInfo="@room" ShowOwnProfile="@(RoomType == "Room")"></RoomListItem>
+ <RoomListItem RoomInfo="@room" ShowOwnProfile="@(RoomType == "Room")" Homeserver="@Homeserver"/>
@* @if (RoomVersionDangerLevel(room) != 0 && *@
@* (room.StateEvents.FirstOrDefault(x=>x.Type == "m.room.power_levels")?.TypedContent is RoomPowerLevelEventContent powerLevels && powerLevels.UserHasPermission(Homeserver.UserId, "m.room.tombstone"))) { *@
@* <MatrixUtils.Web.Shared.SimpleComponents.LinkButton Color="@(RoomVersionDangerLevel(room) == 2 ? "#ff0000" : "#ff8800")" href="@($"/Rooms/Create?Import={room.Room.RoomId}")">Upgrade room</MatrixUtils.Web.Shared.SimpleComponents.LinkButton> *@
@@ -14,10 +14,11 @@
<LinkButton href="@($"/Rooms/{room.Room.RoomId}/Timeline")">View timeline</LinkButton>
<LinkButton href="@($"/Rooms/{room.Room.RoomId}/State/View")">View state</LinkButton>
<LinkButton href="@($"/Rooms/{room.Room.RoomId}/State/Edit")">Edit state</LinkButton>
+ <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Upgrade")" Color="#888800">Upgrade/replace room</LinkButton>
<LinkButton href="@($"/Tools/LeaveRoom?roomId={room.Room.RoomId}")" Color="#FF0000">Leave room</LinkButton>
@if (room.CreationEventContent?.Type == "m.space") {
- <RoomListSpace Space="@room"></RoomListSpace>
+ <RoomListSpace Space="@room" Homeserver="@Homeserver"/>
}
else if (room.CreationEventContent?.Type == "support.feline.policy.lists.msc.v1" || RoomType == "org.matrix.mjolnir.policy") {
<LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")">Manage policies</LinkButton>
@@ -35,9 +36,9 @@
[Parameter]
public UserProfileResponse? GlobalProfile { get; set; }
- [CascadingParameter]
- public AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!;
-
+ [Parameter]
+ public AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+
private string RoomType => Category.Key;
private List<RoomInfo> Rooms => Category.Value;
diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
index 6954990..27f0499 100644
--- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
+++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
@@ -35,15 +35,18 @@
set => _breadcrumbs = value;
}
+ [Parameter]
+ public required AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
private ObservableCollection<RoomInfo> Children { get; set; } = new();
private Collection<RoomInfo> Unjoined { get; set; } = new();
protected override async Task OnInitializedAsync() {
if (Breadcrumbs == null) throw new ArgumentNullException(nameof(Breadcrumbs));
+ if (Homeserver is null) throw new ArgumentNullException(nameof(Homeserver));
await Task.Delay(Random.Shared.Next(1000, 10000));
var rooms = Space.Room.AsSpace.GetChildrenAsync();
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
- var joinedRooms = await hs.GetJoinedRooms();
+ var joinedRooms = await Homeserver.GetJoinedRooms();
await foreach (var room in rooms) {
if (Breadcrumbs.Contains(room.RoomId)) continue;
var roomInfo = KnownRooms.FirstOrDefault(x => x.Room.RoomId == room.RoomId);
@@ -51,10 +54,12 @@
roomInfo = new RoomInfo(room);
KnownRooms.Add(roomInfo);
}
- if(joinedRooms.Any(x=>x.RoomId == room.RoomId))
+
+ if (joinedRooms.Any(x => x.RoomId == room.RoomId))
Children.Add(roomInfo);
else Unjoined.Add(roomInfo);
}
+
await base.OnInitializedAsync();
}
diff --git a/MatrixUtils.Web/Shared/RoomListItem.razor b/MatrixUtils.Web/Shared/RoomListItem.razor
index bfaa900..2d85f64 100644
--- a/MatrixUtils.Web/Shared/RoomListItem.razor
+++ b/MatrixUtils.Web/Shared/RoomListItem.razor
@@ -1,19 +1,26 @@
+@using ArcaneLibs
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.Responses
@using MatrixUtils.Abstractions
@using MatrixUtils.Web.Classes.Constants
@if (RoomInfo is not null) {
<div class="roomListItem @(HasDangerousRoomVersion ? "dangerousRoomVersion" : HasOldRoomVersion ? "oldRoomVersion" : "")" id="@RoomInfo.Room.RoomId">
@if (OwnMemberState != null) {
- @* Class="@("avatar32" + (OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? " highlightChange" : "") + (ChildContent is not null ? " vcenter" : ""))" *@
- <MxcImage Homeserver="hs" Circular="true" Height="32" Width="32" MxcUri="@(OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl)"/>
+ <MxcAvatar Homeserver="@Homeserver" Circular="true" Size="32" MxcUri="@(OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl)"/>
<span class="centerVertical border75 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "")">
@(OwnMemberState?.DisplayName ?? GlobalProfile?.DisplayName ?? "Loading...")
</span>
<span class="centerVertical noLeftPadding">-></span>
}
- <MxcImage Circular="true" Height="32" Width="32" MxcUri="@RoomInfo.RoomIcon" Style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/>
+ @* <MxcImage Circular="true" Height="32" Width="32" MxcUri="@RoomInfo.RoomIcon" Style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/> *@
+
+ @if (!string.IsNullOrWhiteSpace(RoomInfo.RoomIcon)) {
+ <MxcAvatar Homeserver="@Homeserver" Circular="true" Size="32" MxcUri="@RoomInfo.RoomIcon"/>
+ }
+ else {
+ <img src="@Identicon" width="32" height="32" style="border-radius: 50%;"/>
+ }
<div class="inlineBlock">
<span class="centerVertical">@RoomInfo.RoomName</span>
@if (ChildContent is not null) {
@@ -42,8 +49,6 @@ else {
}
}
-
-
[Parameter]
public bool ShowOwnProfile { get; set; } = false;
@@ -61,27 +66,36 @@ else {
OnParametersSetAsync();
}
}
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric? Homeserver { get; set; }
private bool HasOldRoomVersion { get; set; } = false;
private bool HasDangerousRoomVersion { get; set; } = false;
+ private string Identicon { get; set; }
+
+ private static SvgIdenticonGenerator _identiconGenerator = new SvgIdenticonGenerator();
+
private static SemaphoreSlim _semaphoreSlim = new(8);
private RoomInfo? _roomInfo;
private bool _loadData = false;
- private static AuthenticatedHomeserverGeneric? hs { get; set; }
private bool _hooked;
-
+
private async Task RoomInfoChanged() {
+ if (RoomInfo is null) return;
+ Identicon = _identiconGenerator.GenerateAsDataUri(RoomInfo.Room.RoomId);
+
RoomInfo.PropertyChanged += async (_, a) => {
if (a.PropertyName == nameof(RoomInfo.CreationEventContent)) {
await CheckRoomVersion();
}
-
+
StateHasChanged();
};
}
-
+
// protected override async Task OnParametersSetAsync() {
// if (RoomInfo != null) {
// if (!_hooked) {
@@ -127,21 +141,24 @@ else {
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
- hs ??= await RMUStorage.GetCurrentSessionOrNavigate();
- if (hs is null) return;
+ // hs ??= await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ // if (hs is null) return;
+ if (Homeserver is null) {
+ Console.WriteLine($"RoomListItem called without homeserver");
+ }
await CheckRoomVersion();
}
private async Task LoadOwnProfile() {
if (!ShowOwnProfile) return;
try {
- // OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.UserId)).TypedContent as RoomMemberEventContent;
- GlobalProfile ??= await hs.GetProfileAsync(hs.UserId);
+ // OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.UserId)).TypedContent as RoomMemberEventContent;
+ GlobalProfile ??= await Homeserver.GetProfileAsync(Homeserver.UserId);
}
catch (MatrixException e) {
if (e is { ErrorCode: "M_FORBIDDEN" }) {
- Console.WriteLine($"Failed to get profile for {hs.UserId}: {e.Message}");
+ Console.WriteLine($"Failed to get profile for {Homeserver.UserId}: {e.Message}");
ShowOwnProfile = false;
}
else {
@@ -151,8 +168,8 @@ else {
}
private async Task CheckRoomVersion() {
- if (RoomInfo?.CreationEventContent is null) return;
-
+ if (RoomInfo?.CreationEventContent is null) return;
+
var ce = RoomInfo.CreationEventContent;
if (int.TryParse(ce.RoomVersion, out var rv)) {
if (rv < 10)
@@ -163,7 +180,7 @@ else {
if (RoomConstants.DangerousRoomVersions.Contains(ce.RoomVersion)) {
HasDangerousRoomVersion = true;
- // RoomName = "Dangerous room: " + RoomName;
+ // RoomName = "Dangerous room: " + RoomName;
}
}
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
index 08aeffe..f107eb3 100644
--- a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
+++ b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
@@ -1,5 +1,5 @@
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.Responses
<h3>BaseTimelineItem</h3>
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor
index 0488e36..d1984dd 100644
--- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor
@@ -1,5 +1,5 @@
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@inherits BaseTimelineItem
@if (currentEventContent is not null) {
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor
index bdd6104..5d09603 100644
--- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor
@@ -1,5 +1,5 @@
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@inherits BaseTimelineItem
@if (currentEventContent is not null) {
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
index 3b18b95..e5a5650 100644
--- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
@@ -1,5 +1,5 @@
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.Responses
@inherits BaseTimelineItem
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
index 81956b0..98b5a6d 100644
--- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
@@ -15,7 +15,7 @@
}
case "m.image": {
<i>@currentEventContent.Body</i><br/>
- <img src="@Homeserver.ResolveMediaUri(currentEventContent.Url)">
+ @* <img src="@Homeserver.ResolveMediaUri(currentEventContent.Url)"> *@
break;
}
default: {
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
index f3e6c7e..aeb987a 100644
--- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
@@ -1,5 +1,5 @@
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@inherits BaseTimelineItem
<i>
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor
index 63594a9..c342c83 100644
--- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor
@@ -1,5 +1,5 @@
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@inherits BaseTimelineItem
@if (currentEventContent is not null) {
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor
index f70d563..467c644 100644
--- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor
@@ -1,5 +1,5 @@
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@inherits BaseTimelineItem
@if (currentEventContent is not null) {
diff --git a/MatrixUtils.Web/Shared/UserListItem.razor b/MatrixUtils.Web/Shared/UserListItem.razor
index d4652b2..5084807 100644
--- a/MatrixUtils.Web/Shared/UserListItem.razor
+++ b/MatrixUtils.Web/Shared/UserListItem.razor
@@ -23,13 +23,14 @@
[Parameter]
public string UserId { get; set; }
- private AuthenticatedHomeserverGeneric _homeserver = null!;
+ [Parameter]
+ public AuthenticatedHomeserverGeneric _homeserver { get; set; }
private SvgIdenticonGenerator _identiconGenerator = new();
protected override async Task OnInitializedAsync() {
- _homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
- if (_homeserver is null) return;
+ // _homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ // if (_homeserver is null) return;
if (User == null) {
if (UserId == null) {
diff --git a/MatrixUtils.Web/_Imports.razor b/MatrixUtils.Web/_Imports.razor
index 81c7874..b5a1316 100644
--- a/MatrixUtils.Web/_Imports.razor
+++ b/MatrixUtils.Web/_Imports.razor
@@ -1,13 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
-@* @using Blazored.LocalStorage *@
@using LibMatrix.Services
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
-@* @using Microsoft.AspNetCore.Components.Web.Virtualization *@
@using Microsoft.AspNetCore.Components.WebAssembly.Http
-@* @using Microsoft.JSInterop *@
@using MatrixUtils.Web
@using MatrixUtils.Web.Classes
@using MatrixUtils.Web.Shared
@@ -16,8 +13,8 @@
@using Microsoft.JSInterop
@inject NavigationManager NavigationManager
-@inject RMUStorageWrapper RMUStorage
-@inject HomeserverProviderService hsProvider
+@inject RmuSessionStore sessionStore
+@inject HomeserverProviderService HsProvider
@inject TieredStorageService TieredStorage
-@inject HomeserverResolverService hsResolver
-@inject IJSRuntime JSRuntime
+@inject HomeserverResolverService HsResolver
+@inject IJSRuntime JsRuntime
diff --git a/MatrixUtils.Web/wwwroot/index.html b/MatrixUtils.Web/wwwroot/index.html
index 5182193..7425de2 100644
--- a/MatrixUtils.Web/wwwroot/index.html
+++ b/MatrixUtils.Web/wwwroot/index.html
@@ -57,6 +57,22 @@
height: window.innerHeight
};
}
+
+ setImageStream = async (element, imageStream) => {
+ if(!(element instanceof HTMLElement)) {
+ console.error("Element is not an HTMLElement", element);
+ return;
+ }
+
+ const arrayBuffer = await imageStream.arrayBuffer();
+ const blob = new Blob([arrayBuffer]);
+ const url = URL.createObjectURL(blob);
+ const image = document.getElementById(imageElementId);
+ image.onload = () => {
+ URL.revokeObjectURL(url);
+ }
+ image.src = url;
+ }
</script>
<script src="_framework/blazor.webassembly.js"></script>
<!-- <script>navigator.serviceWorker.register('service-worker.js');</script>-->
diff --git a/MatrixUtils.sln b/MatrixUtils.sln
new file mode 100644
index 0000000..5fb0c1f
--- /dev/null
+++ b/MatrixUtils.sln
@@ -0,0 +1,215 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+#
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Web", "MatrixUtils.Web\MatrixUtils.Web.csproj", "{D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Web.Server", "MatrixUtils.Web.Server\MatrixUtils.Web.Server.csproj", "{F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Desktop", "MatrixUtils.Desktop\MatrixUtils.Desktop.csproj", "{27C08A4F-5AF0-4C2C-AFCB-050E3388C116}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.LibDMSpace", "MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj", "{EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Abstractions", "MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj", "{FE20ED20-0D55-4D74-822B-E2AC7A54C487}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LibMatrix", "LibMatrix", "{933DC8A6-8B1F-46BF-9046-4B636AA46469}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "ArcaneLibs", "{84BE90C4-2FDE-4A48-B154-58926EF24846}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Tests", "LibMatrix\ArcaneLibs\ArcaneLibs.Tests\ArcaneLibs.Tests.csproj", "{EC5536AB-0613-4CB5-B22B-822A3DBB112A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "LibMatrix\ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{0C542A8E-54B6-4A20-B7A9-8C7190A0C232}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "LibMatrix\ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{48AF8AC7-5F59-4401-B173-523D37FDD7A8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "LibMatrix\ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "LibMatrix\ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "LibMatrix\ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{D6315791-949B-4501-AA95-50516DE899C1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "LibMatrix\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{03466515-77CC-49E4-90E5-9A21EDD0A644}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{0336306C-285A-4810-9253-5C5F0373992E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix\LibMatrix.csproj", "{D7E5B226-114C-4747-9277-A4D6341A16FE}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B37F87A8-B5E2-4724-800C-F5D9A91F35C7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "LibMatrix\Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{D293AFEC-8322-4FEC-8425-143B5FE10D0F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{80828C75-9C5B-442F-86A4-8CE9D85E811C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "LibMatrix\Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{FA6A9923-419A-40E1-8A32-30DD906E5025}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "LibMatrix\Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.E2eeTestKit", "LibMatrix\Utilities\LibMatrix.E2eeTestKit\LibMatrix.E2eeTestKit.csproj", "{CC87DFFB-EE19-4147-9212-4FAF16D79AD5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "LibMatrix\Utilities\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{DBCE6260-052E-46F9-ACCD-059AA51B8A48}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.JsonSerializerContextGenerator", "LibMatrix\Utilities\LibMatrix.JsonSerializerContextGenerator\LibMatrix.JsonSerializerContextGenerator.csproj", "{7AA3CDF9-D1F6-4A12-BA47-EB721F353701}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.TestDataGenerator", "LibMatrix\Utilities\LibMatrix.TestDataGenerator\LibMatrix.TestDataGenerator.csproj", "{D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{72D44C6C-1BC7-4310-B1A9-1169C0812E33}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.DmSpaced", "MatrixUtils.DmSpaced\MatrixUtils.DmSpaced.csproj", "{CDBE012E-B48B-4F9D-8CA4-99F6328E9630}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MxApiExtensions", "MxApiExtensions", "{0641F1C8-8518-4C67-B385-832745C063FD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions.Classes.LibMatrix", "MxApiExtensions\MxApiExtensions.Classes.LibMatrix\MxApiExtensions.Classes.LibMatrix.csproj", "{3BD05B05-86DE-4680-A7A0-5A326E41E776}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions.Classes", "MxApiExtensions\MxApiExtensions.Classes\MxApiExtensions.Classes.csproj", "{98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions", "MxApiExtensions\MxApiExtensions\MxApiExtensions.csproj", "{44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{CEECE820-1BA9-4E29-8668-25967B3E712B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|Any CPU.Build.0 = Release|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|Any CPU.Build.0 = Release|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|Any CPU.Build.0 = Release|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|Any CPU.Build.0 = Release|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {84BE90C4-2FDE-4A48-B154-58926EF24846} = {933DC8A6-8B1F-46BF-9046-4B636AA46469}
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A} = {84BE90C4-2FDE-4A48-B154-58926EF24846}
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE} = {84BE90C4-2FDE-4A48-B154-58926EF24846}
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232} = {84BE90C4-2FDE-4A48-B154-58926EF24846}
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8} = {84BE90C4-2FDE-4A48-B154-58926EF24846}
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D} = {84BE90C4-2FDE-4A48-B154-58926EF24846}
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72} = {84BE90C4-2FDE-4A48-B154-58926EF24846}
+ {D6315791-949B-4501-AA95-50516DE899C1} = {84BE90C4-2FDE-4A48-B154-58926EF24846}
+ {03466515-77CC-49E4-90E5-9A21EDD0A644} = {84BE90C4-2FDE-4A48-B154-58926EF24846}
+ {0336306C-285A-4810-9253-5C5F0373992E} = {933DC8A6-8B1F-46BF-9046-4B636AA46469}
+ {D7E5B226-114C-4747-9277-A4D6341A16FE} = {933DC8A6-8B1F-46BF-9046-4B636AA46469}
+ {B37F87A8-B5E2-4724-800C-F5D9A91F35C7} = {933DC8A6-8B1F-46BF-9046-4B636AA46469}
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F} = {B37F87A8-B5E2-4724-800C-F5D9A91F35C7}
+ {80828C75-9C5B-442F-86A4-8CE9D85E811C} = {933DC8A6-8B1F-46BF-9046-4B636AA46469}
+ {FA6A9923-419A-40E1-8A32-30DD906E5025} = {80828C75-9C5B-442F-86A4-8CE9D85E811C}
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0} = {80828C75-9C5B-442F-86A4-8CE9D85E811C}
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5} = {80828C75-9C5B-442F-86A4-8CE9D85E811C}
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48} = {80828C75-9C5B-442F-86A4-8CE9D85E811C}
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701} = {80828C75-9C5B-442F-86A4-8CE9D85E811C}
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9} = {80828C75-9C5B-442F-86A4-8CE9D85E811C}
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33} = {80828C75-9C5B-442F-86A4-8CE9D85E811C}
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776} = {0641F1C8-8518-4C67-B385-832745C063FD}
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53} = {0641F1C8-8518-4C67-B385-832745C063FD}
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684} = {0641F1C8-8518-4C67-B385-832745C063FD}
+ EndGlobalSection
+EndGlobal
diff --git a/MxApiExtensions b/MxApiExtensions
-Subproject 86e41aa749d961c5731ea52e570cf0f9e8f8d3a
+Subproject b4ef05afcfac87ae197ae69bdbae93c3ca4d46b
diff --git a/global.json b/global.json
index ecc6db8..6d77f62 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.0",
+ "version": "9.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
}
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index 1abe9e7..7c5086f 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -11,4 +11,5 @@ BASE_DIR=`pwd`
rm -rf **/bin/Release
cd MatrixUtils.Web
dotnet publish -c Release
-rsync --delete -raP bin/Release/net8.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/
+dotnet restore # restore debug deps
+rsync --delete -raP bin/Release/net9.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/
|