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/vcs.xml b/.idea/.idea.MatrixRoomUtils/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/.idea.MatrixRoomUtils/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
- <component name="VcsDirectoryMappings">
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
- </component>
-</project>
\ 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..4520708 100644
--- a/.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml
+++ b/.idea/.idea.MatrixUtils/.idea/indexLayout.xml
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.MatrixUtils/.idea/vcs.xml b/.idea/.idea.MatrixUtils/.idea/vcs.xml
new file mode 100644
index 0000000..df05d42
--- /dev/null
+++ b/.idea/.idea.MatrixUtils/.idea/vcs.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ <mapping directory="$PROJECT_DIR$/LibMatrix" vcs="Git" />
+ <mapping directory="$PROJECT_DIR$/LibMatrix/ArcaneLibs" vcs="Git" />
+ <mapping directory="$PROJECT_DIR$/MxApiExtensions" vcs="Git" />
+ </component>
+</project>
\ No newline at end of file
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..a174f1f
--- /dev/null
+++ b/Benchmarks/Benchmarks.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net10.0</TargetFramework>
+ <LangVersion>preview</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <!-- <PublishAot>true</PublishAot>-->
+ <InvariantGlobalization>true</InvariantGlobalization>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="BenchmarkDotNet" Version="0.15.4" />
+ </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 1db452c75de1e25a9a2a8fd4fe2a04a2e1047f2
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..751aa5d 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>net10.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/App.axaml.cs b/MatrixUtils.Desktop/App.axaml.cs
index 3a106ab..8a5d3e2 100644
--- a/MatrixUtils.Desktop/App.axaml.cs
+++ b/MatrixUtils.Desktop/App.axaml.cs
@@ -15,7 +15,6 @@ public partial class App : Application {
public override void OnFrameworkInitializationCompleted() {
host = Host.CreateDefaultBuilder().ConfigureServices((ctx, services) => {
services.AddSingleton<RMUDesktopConfiguration>();
- services.AddSingleton<SentryService>();
services.AddSingleton<TieredStorageService>(x =>
new TieredStorageService(
cacheStorageProvider: new FileStorageProvider(x.GetService<RMUDesktopConfiguration>()!.CacheStoragePath),
@@ -40,10 +39,10 @@ public partial class App : Application {
var scope = scopeFac.CreateScope();
desktop.MainWindow = scope.ServiceProvider.GetRequiredService<MainWindow>();
}
-
- if(Environment.GetEnvironmentVariable("AVALONIA_THEME")?.Equals("dark", StringComparison.OrdinalIgnoreCase) ?? false)
+
+ if (Environment.GetEnvironmentVariable("AVALONIA_THEME")?.Equals("dark", StringComparison.OrdinalIgnoreCase) ?? false)
RequestedThemeVariant = ThemeVariant.Dark;
-
+
base.OnFrameworkInitializationCompleted();
}
}
\ No newline at end of file
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/MainWindow.axaml.cs b/MatrixUtils.Desktop/MainWindow.axaml.cs
index 9c783e4..a1eef56 100644
--- a/MatrixUtils.Desktop/MainWindow.axaml.cs
+++ b/MatrixUtils.Desktop/MainWindow.axaml.cs
@@ -14,7 +14,7 @@ public partial class MainWindow : Window {
private readonly RMUDesktopConfiguration _configuration;
public static MainWindow Instance { get; private set; } = null!;
- public MainWindow(ILogger<MainWindow> logger, IServiceScopeFactory scopeFactory, SentryService _) {
+ public MainWindow(ILogger<MainWindow> logger, IServiceScopeFactory scopeFactory) {
Instance = this;
_logger = logger;
_scopeFactory = scopeFactory;
diff --git a/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj b/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj
index ce009d5..419ae88 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>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
@@ -10,31 +10,28 @@
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
-<!-- <PublishTrimmed>true</PublishTrimmed>-->
-<!-- <PublishReadyToRun>true</PublishReadyToRun>-->
-<!-- <PublishSingleFile>true</PublishSingleFile>-->
-<!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>-->
-<!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>-->
-<!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>-->
+ <!-- <PublishTrimmed>true</PublishTrimmed>-->
+ <!-- <PublishReadyToRun>true</PublishReadyToRun>-->
+ <!-- <PublishSingleFile>true</PublishSingleFile>-->
+ <!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>-->
+ <!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>-->
+ <!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>-->
</PropertyGroup>
<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.3.8"/>
+ <PackageReference Include="Avalonia.Desktop" Version="11.3.8"/>
+ <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.8"/>
+ <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.8"/>
<!--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"/>
</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.3.0.6"/>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107"/>
</ItemGroup>
<ItemGroup>
<Content Include="appsettings*.json">
@@ -45,6 +42,6 @@
</Content>
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj" />
+ <ProjectReference Include="..\MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj"/>
</ItemGroup>
</Project>
diff --git a/MatrixUtils.Desktop/RMUDesktopConfiguration.cs b/MatrixUtils.Desktop/RMUDesktopConfiguration.cs
index 62646ca..f9515f6 100644
--- a/MatrixUtils.Desktop/RMUDesktopConfiguration.cs
+++ b/MatrixUtils.Desktop/RMUDesktopConfiguration.cs
@@ -21,7 +21,6 @@ public class RMUDesktopConfiguration {
public string DataStoragePath { get; set; } = "";
public string CacheStoragePath { get; set; } = "";
- public string? SentryDsn { get; set; }
private static string ExpandPath(string path, bool retry = true) {
_logger.LogInformation("Expanding path `{}`", path);
@@ -44,4 +43,4 @@ public class RMUDesktopConfiguration {
return path;
}
-}
+}
\ No newline at end of file
diff --git a/MatrixUtils.Desktop/SentryService.cs b/MatrixUtils.Desktop/SentryService.cs
deleted file mode 100644
index c965632..0000000
--- a/MatrixUtils.Desktop/SentryService.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Sentry;
-
-namespace MatrixUtils.Desktop;
-
-public class SentryService : IDisposable {
- private IDisposable? _sentrySdkDisposable;
- public SentryService(IServiceScopeFactory scopeFactory, ILogger<SentryService> logger) {
- var config = scopeFactory.CreateScope().ServiceProvider.GetRequiredService<RMUDesktopConfiguration>();
- if (config.SentryDsn is null) {
- logger.LogWarning("Sentry DSN is not set, skipping Sentry initialisation");
- return;
- }
- _sentrySdkDisposable = SentrySdk.Init(o => {
- o.Dsn = config.SentryDsn;
- // When configuring for the first time, to see what the SDK is doing:
- o.Debug = true;
- // Set traces_sample_rate to 1.0 to capture 100% of transactions for performance monitoring.
- // We recommend adjusting this value in production.
- o.TracesSampleRate = 1.0;
- // Enable Global Mode if running in a client app
- o.IsGlobalModeEnabled = true;
- });
- }
-
- /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
- public void Dispose() => _sentrySdkDisposable?.Dispose();
-}
diff --git a/MatrixUtils.Desktop/appsettings.Development.json b/MatrixUtils.Desktop/appsettings.Development.json
index a1add03..baec0e2 100644
--- a/MatrixUtils.Desktop/appsettings.Development.json
+++ b/MatrixUtils.Desktop/appsettings.Development.json
@@ -1,14 +1,13 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Debug",
- "System": "Information",
- "Microsoft": "Information"
- }
- },
- "RMUDesktop": {
- "DataStoragePath": "rmu-desktop/data",
- "CacheStoragePath": "rmu-desktop/cache",
- "SentryDsn": "https://a41e99dd2fdd45f699c432b21ebce632@sentry.thearcanebrony.net/15"
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
}
+ },
+ "RMUDesktop": {
+ "DataStoragePath": "rmu-desktop/data",
+ "CacheStoragePath": "rmu-desktop/cache"
+ }
}
diff --git a/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj
index 4b0f599..96c3bff 100644
--- a/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj
+++ b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj
@@ -1,31 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <OutputType>Exe</OutputType>
- <TargetFramework>net8.0</TargetFramework>
- <LangVersion>preview</LangVersion>
- <ImplicitUsings>enable</ImplicitUsings>
- <Nullable>enable</Nullable>
- <PublishAot>false</PublishAot>
- <InvariantGlobalization>true</InvariantGlobalization>
- <!-- <PublishTrimmed>true</PublishTrimmed>-->
- <!-- <PublishReadyToRun>true</PublishReadyToRun>-->
- <!-- <PublishSingleFile>true</PublishSingleFile>-->
- <!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>-->
- <!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>-->
- <!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>-->
- </PropertyGroup>
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net10.0</TargetFramework>
+ <LangVersion>preview</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <PublishAot>false</PublishAot>
+ <InvariantGlobalization>true</InvariantGlobalization>
+ <!-- <PublishTrimmed>true</PublishTrimmed>-->
+ <!-- <PublishReadyToRun>true</PublishReadyToRun>-->
+ <!-- <PublishSingleFile>true</PublishSingleFile>-->
+ <!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>-->
+ <!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>-->
+ <!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>-->
+ </PropertyGroup>
- <ItemGroup>
- <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj" />
- </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj"/>
+ </ItemGroup>
- <ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
- </ItemGroup>
- <ItemGroup>
- <Content Include="appsettings*.json">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107"/>
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="appsettings*.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
</Project>
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..1186b6c 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;
@@ -60,10 +58,10 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI
var (userId, dmRooms) = entry;
DMSpaceChildLayer? layer = await GetStateOrNullAsync<DMSpaceChildLayer>(DMSpaceChildLayer.EventId, userId.UrlEncode()) ?? await CreateLayer(userId);
return (entry, layer);
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var ((userId, dmRooms), layer) in layerTasks) {
- var space = Homeserver.GetRoom(layer.SpaceId).AsSpace;
+ var space = Homeserver.GetRoom(layer.SpaceId).AsSpace();
foreach (var roomid in dmRooms) {
var dri = new DMRoomInfo() {
AttributedUser = userId
@@ -119,12 +117,11 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI
catch {
return (x, null);
}
-
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var (layer, profile) in getProfileTasks) {
if (profile is null) continue;
var layerContent = layer.TypedContent as DMSpaceChildLayer;
- var space = Homeserver.GetRoom(layerContent!.SpaceId).AsSpace;
+ var space = Homeserver.GetRoom(layerContent!.SpaceId).AsSpace();
try {
await space.SendStateEventAsync(RoomAvatarEventContent.EventId, "", new RoomAvatarEventContent() {
@@ -142,7 +139,7 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI
private async Task UpdateLayer(DMSpaceChildLayer layer, string mxid) {
UserProfileResponse? profile = null;
- var space = Homeserver.GetRoom(layer.SpaceId).AsSpace;
+ var space = Homeserver.GetRoom(layer.SpaceId).AsSpace();
if (string.IsNullOrWhiteSpace(layer.OverrideAvatar) || string.IsNullOrWhiteSpace(layer.OverrideName)) {
try {
diff --git a/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj b/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj
index 72c1666..225b264 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>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LinkIncremental>true</LinkIncremental>
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.RoomUpgradeCLI/Commands/DevCommands/DevDeleteAllRoomsCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteAllRoomsCommand.cs
new file mode 100644
index 0000000..abae488
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteAllRoomsCommand.cs
@@ -0,0 +1,32 @@
+using LibMatrix.Homeservers;
+
+namespace MatrixUtils.RoomUpgradeCLI.Commands;
+
+public class DevDeleteAllRoomsCommand(ILogger<DevDeleteAllRoomsCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService {
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ var synapse = hs as AuthenticatedHomeserverSynapse;
+ await foreach (var room in synapse.Admin.SearchRoomsAsync())
+ {
+ try
+ {
+ await synapse.Admin.DeleteRoom(room.RoomId, new() { ForcePurge = true });
+ Console.WriteLine($"Deleted room: {room.RoomId}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to delete room {room.RoomId}: {ex.Message}");
+ }
+ }
+
+ await host.StopAsync(cancellationToken);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) { }
+
+ private async Task PrintHelp() {
+ Console.WriteLine("Usage: execute [filename]");
+ Console.WriteLine("Options:");
+ Console.WriteLine(" --help Show this help message");
+ await host.StopAsync();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteRoomCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteRoomCommand.cs
new file mode 100644
index 0000000..10d667f
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteRoomCommand.cs
@@ -0,0 +1,33 @@
+using LibMatrix.Homeservers;
+
+namespace MatrixUtils.RoomUpgradeCLI.Commands;
+
+public class DevDeleteRoomCommand(ILogger<DevDeleteRoomCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService {
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ var synapse = hs as AuthenticatedHomeserverSynapse;
+ if (ctx.Args.Length == 2) {
+ var room = synapse.GetRoom(ctx.Args[1]);
+ await synapse.Admin.DeleteRoom(room.RoomId, new() { Purge = true });
+ }
+ else {
+ string line;
+ do {
+ line = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(line)) continue;
+ var room = synapse.GetRoom(line);
+ await synapse.Admin.DeleteRoom(room.RoomId, new() { Purge = true });
+ } while (line is not null);
+ }
+
+ await host.StopAsync(cancellationToken);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) { }
+
+ private async Task PrintHelp() {
+ Console.WriteLine("Usage: execute [filename]");
+ Console.WriteLine("Options:");
+ Console.WriteLine(" --help Show this help message");
+ await host.StopAsync();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevGetRoomDirStateCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevGetRoomDirStateCommand.cs
new file mode 100644
index 0000000..7ff7b6a
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevGetRoomDirStateCommand.cs
@@ -0,0 +1,31 @@
+using System.Web;
+using LibMatrix.Homeservers;
+
+namespace MatrixUtils.RoomUpgradeCLI.Commands;
+
+public class DevGetRoomDirStateCommand(ILogger<DevGetRoomDirStateCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService {
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ var synapse = hs as AuthenticatedHomeserverSynapse;
+ if (ctx.Args.Length == 2) {
+ var res = await hs.ClientHttpClient.GetAsync(" /_matrix/client/v3/directory/list/room/" + HttpUtility.UrlEncode(ctx.Args[1]));
+ if (res.IsSuccessStatusCode) {
+ var data = await res.Content.ReadAsStringAsync();
+ Console.WriteLine("Room Directory State for " + ctx.Args[1] + ":");
+ Console.WriteLine(data);
+ } else {
+ Console.WriteLine("Failed to get room directory state for " + ctx.Args[1] + ": " + res.ReasonPhrase);
+ }
+ }
+
+ await host.StopAsync(cancellationToken);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) { }
+
+ private async Task PrintHelp() {
+ Console.WriteLine("Usage: execute [filename]");
+ Console.WriteLine("Options:");
+ Console.WriteLine(" --help Show this help message");
+ await host.StopAsync();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/ExecuteCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/ExecuteCommand.cs
new file mode 100644
index 0000000..41c8cca
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Commands/ExecuteCommand.cs
@@ -0,0 +1,63 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+
+namespace MatrixUtils.RoomUpgradeCLI.Commands;
+
+public class ExecuteCommand(ILogger<ExecuteCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService {
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ if (ctx.Args.Length <= 1) {
+ await PrintHelp();
+ return;
+ }
+ var filename = ctx.Args[1];
+ if (filename.StartsWith("--")) {
+ Console.WriteLine("Filename cannot start with --, please provide a valid filename.");
+ await PrintHelp();
+ }
+
+ if (Directory.Exists(filename)) {
+ await ExecuteDirectory(filename);
+ }
+ else if (File.Exists(filename)) {
+ await ExecuteFile(filename);
+ }
+ else {
+ Console.WriteLine($"File or directory {filename} does not exist.");
+ await PrintHelp();
+ }
+
+ await host.StopAsync(cancellationToken);
+ }
+
+ public async Task ExecuteFile(string filename) {
+ var rbj = await JsonSerializer.DeserializeAsync<JsonObject>(File.OpenRead(filename));
+ var rb = rbj.ContainsKey(nameof(RoomUpgradeBuilder.OldRoomId))
+ ? rbj.Deserialize<RoomUpgradeBuilder>()
+ : rbj.Deserialize<RoomBuilder>();
+ Console.WriteLine($"Executing room builder file of type {rb.GetType().Name}...");
+ await rb!.Create(hs);
+ }
+
+ public async Task ExecuteDirectory(string dirName) {
+ if (!Directory.Exists(dirName)) {
+ Console.WriteLine($"Directory {dirName} does not exist.");
+ return;
+ }
+ var files = Directory.GetFiles(dirName, "*.json");
+ foreach (var file in files) {
+ Console.WriteLine($"Executing file: {file}");
+ await ExecuteFile(file);
+ }
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) { }
+
+ private async Task PrintHelp() {
+ Console.WriteLine("Usage: execute [filename]");
+ Console.WriteLine("Options:");
+ Console.WriteLine(" --help Show this help message");
+ await host.StopAsync();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/ImportUpgradeStateCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/ImportUpgradeStateCommand.cs
new file mode 100644
index 0000000..960905b
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Commands/ImportUpgradeStateCommand.cs
@@ -0,0 +1,35 @@
+using System.Text.Json;
+using ArcaneLibs.Extensions;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+
+namespace MatrixUtils.RoomUpgradeCLI.Commands;
+
+public class ImportUpgradeStateCommand(ILogger<ImportUpgradeStateCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService {
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ if (ctx.Args.Length <= 1) {
+ await PrintHelp();
+ return;
+ }
+ var filename = ctx.Args[1];
+ if (filename.StartsWith("--")) {
+ Console.WriteLine("Filename cannot start with --, please provide a valid filename.");
+ await PrintHelp();
+ }
+
+ var rb = await JsonSerializer.DeserializeAsync<RoomUpgradeBuilder>(File.OpenRead(filename));
+ await rb!.ImportAsync(hs.GetRoom(rb.OldRoomId));
+ await File.WriteAllTextAsync(filename, rb.ToJson(), cancellationToken);
+
+ await host.StopAsync(cancellationToken);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) { }
+
+ private async Task PrintHelp() {
+ Console.WriteLine("Usage: import-upgrade-state [filename]");
+ Console.WriteLine("Options:");
+ Console.WriteLine(" --help Show this help message");
+ await host.StopAsync();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/ModifyCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/ModifyCommand.cs
new file mode 100644
index 0000000..3860448
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Commands/ModifyCommand.cs
@@ -0,0 +1,39 @@
+using System.Text.Json;
+using ArcaneLibs.Extensions;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using MatrixUtils.RoomUpgradeCLI.Extensions;
+
+namespace MatrixUtils.RoomUpgradeCLI.Commands;
+
+public class ModifyCommand(ILogger<ModifyCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService {
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ if (ctx.Args.Length <= 2 || ctx.Args.Contains("--help")) {
+ await PrintHelp();
+ return;
+ }
+
+ var filename = ctx.Args[1];
+ if (filename.StartsWith("--")) {
+ Console.WriteLine("Filename cannot start with --, please provide a valid filename.");
+ await PrintHelp();
+ }
+
+ var rb = ctx.Args.Contains("--upgrade")
+ ? await JsonSerializer.DeserializeAsync<RoomUpgradeBuilder>(File.OpenRead(filename), cancellationToken: cancellationToken)
+ : await JsonSerializer.DeserializeAsync<RoomBuilder>(File.OpenRead(filename), cancellationToken: cancellationToken);
+ await rb!.ApplyRoomUpgradeCLIArgs(hs, ctx.Args[2..], isNewState: false);
+ await File.WriteAllTextAsync(filename, rb.ToJson(), cancellationToken);
+
+ await host.StopAsync(cancellationToken);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) { }
+
+ private async Task PrintHelp() {
+ Console.WriteLine("Usage: new [filename] [options]");
+ Console.WriteLine("Options:");
+
+ await host.StopAsync();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/NewFileCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/NewFileCommand.cs
new file mode 100644
index 0000000..08daf71
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Commands/NewFileCommand.cs
@@ -0,0 +1,78 @@
+using ArcaneLibs.Extensions;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using MatrixUtils.RoomUpgradeCLI.Extensions;
+
+namespace MatrixUtils.RoomUpgradeCLI.Commands;
+
+public class NewFileCommand(ILogger<NewFileCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService {
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ var rb = ctx.Args.Contains("--upgrade") ? new RoomUpgradeBuilder() : new RoomBuilder();
+ if (ctx.Args.Length <= 1) {
+ await PrintHelp();
+ return;
+ }
+ var filename = ctx.Args[1];
+ if (filename.StartsWith("--")) {
+ Console.WriteLine("Filename cannot start with --, please provide a valid filename.");
+ await PrintHelp();
+ }
+ await rb.ApplyRoomUpgradeCLIArgs(hs, ctx.Args[2..], isNewState: true);
+ // check for room membership!
+ if (rb is RoomUpgradeBuilder rub) {
+ try {
+ var room = hs.GetRoom(rub.OldRoomId);
+ var membership = await room.GetStateAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, hs.UserId);
+ }
+ catch (Exception e) {
+ Console.WriteLine("Error checking room membership: " + e.Message);
+ Console.WriteLine("Please ensure you are a member of the room you are trying to upgrade. -- ABORTING --");
+ await host.StopAsync();
+ return;
+ }
+ }
+ await File.WriteAllTextAsync(filename, rb.ToJson(), cancellationToken);
+
+ await host.StopAsync(cancellationToken);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) { }
+
+ private async Task PrintHelp() {
+ Console.WriteLine("Usage: new [filename] [options]");
+ Console.WriteLine("Options:");
+ Console.WriteLine(" --help Show this help message");
+ Console.WriteLine(" --version <version> Set the room version (e.g. 9, 10, 11, 12)");
+ Console.WriteLine("-- New room options --");
+ Console.WriteLine(" --alias <alias> Set the room alias (local part)");
+ Console.WriteLine(" --avatar-url <url> Set the room avatar URL");
+ Console.WriteLine(" --copy-avatar <roomId> Copy the avatar from an existing room");
+ Console.WriteLine(" --copy-powerlevels <roomId> Copy power levels from an existing room");
+ Console.WriteLine(" --invite-admin <userId> Invite a user as an admin (userId must start with '@')");
+ Console.WriteLine(" --invite <userId> Invite a user (userId must start with '@')");
+ Console.WriteLine(" --name <name> Set the room name (can be multiple words)");
+ Console.WriteLine(" --topic <topic> Set the room topic (can be multiple words)");
+ Console.WriteLine(" --federate <true|false> Set whether the room is federatable");
+ Console.WriteLine(" --public Set the room join rule to public");
+ Console.WriteLine(" --invite-only Set the room join rule to invite-only");
+ Console.WriteLine(" --knock Set the room join rule to knock");
+ Console.WriteLine(" --restricted Set the room join rule to restricted");
+ Console.WriteLine(" --knock_restricted Set the room join rule to knock_restricted");
+ Console.WriteLine(" --private Set the room join rule to private");
+ Console.WriteLine(" --join-rule <rule> Set the room join rule (public, invite, knock, restricted, knock_restricted, private)");
+ Console.WriteLine(" --history-visibility <visibility> Set the room history visibility (shared, invited, joined, world_readable)");
+ Console.WriteLine(" --type <type> Set the room type (e.g. m.space, m.room, support.feline.policy.list.msc.v1 etc.)");
+ // upgrade opts
+ Console.WriteLine("-- Upgrade options --");
+ Console.WriteLine(" --upgrade <roomId> Create a room upgrade file instead of a new room file - WARNING: incompatible with non-upgrade options");
+ Console.WriteLine(" --invite-members Invite members during room upgrade");
+ Console.WriteLine(" --invite-powerlevel-users Invite users with power levels during room upgrade");
+ Console.WriteLine(" --migrate-bans Migrate bans during room upgrade");
+ Console.WriteLine(" --migrate-empty-state-events Migrate empty state events during room upgrade");
+ Console.WriteLine(" --upgrade-unstable-values Upgrade unstable values during room upgrade");
+ Console.WriteLine(" --msc4321-policy-list-upgrade <move|transition> Upgrade MSC4321 policy list");
+ Console.WriteLine("WARNING: The --upgrade option is incompatible with options listed under \"New room\", please use the equivalent options in the `modify` command instead.");
+ await host.StopAsync();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/NewFromRoomDirCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/NewFromRoomDirCommand.cs
new file mode 100644
index 0000000..40ab791
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Commands/NewFromRoomDirCommand.cs
@@ -0,0 +1,115 @@
+using ArcaneLibs.Extensions;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using MatrixUtils.RoomUpgradeCLI.Extensions;
+
+namespace MatrixUtils.RoomUpgradeCLI.Commands;
+
+public class NewFromRoomDirCommand(ILogger<NewFromRoomDirCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService {
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ if (ctx.Args.Length <= 1) {
+ await PrintHelp();
+ return;
+ }
+
+ var dirName = ctx.Args[1];
+ if (dirName.StartsWith("--")) {
+ Console.WriteLine("Directory name cannot start with --, please provide a valid directory name.");
+ await PrintHelp();
+ }
+
+ if (Directory.Exists(dirName))
+ Directory.Delete(dirName, true);
+ Directory.CreateDirectory(dirName);
+ List<Task> tasks = [];
+ await foreach (var rooms in hs.EnumeratePublicRoomsAsync().WithCancellation(cancellationToken)) {
+ // foreach (var room in rooms.Chunk) { }
+ tasks.AddRange(rooms.Chunk.Select(x=> ProcessRoom(dirName, x)));
+ }
+ await Task.WhenAll(tasks);
+
+ // var rb = ctx.Args.Contains("--upgrade") ? new RoomUpgradeBuilder() : new RoomBuilder();
+ //
+ // // check for room membership!
+ // if (rb is RoomUpgradeBuilder rub) {
+
+ // }
+ await host.StopAsync(cancellationToken);
+ }
+
+ private async Task ProcessRoom(string dirName, PublicRoomDirectoryResult.PublicRoomListItem roomListItem) {
+ Console.WriteLine(roomListItem.Name ?? roomListItem.RoomId);
+ var room = hs.GetRoom(roomListItem.RoomId);
+ var rb = new RoomUpgradeBuilder() {
+ OldRoomId = roomListItem.RoomId
+ };
+
+ await rb.ApplyRoomUpgradeCLIArgs(hs, ctx.Args[2..], isNewState: true);
+ try {
+ var membership = await room.GetStateAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, hs.UserId);
+ }
+ catch (Exception e) {
+ Console.WriteLine("Error checking room membership: " + e.Message);
+ Console.WriteLine("Please ensure you are a member of the room you are trying to upgrade. -- ABORTING --");
+ await host.StopAsync();
+ return;
+ }
+
+ await rb.ImportAsync(hs.GetRoom(roomListItem.RoomId));
+
+ var validFileNameChars = (roomListItem.Name ?? roomListItem.CanonicalAlias ?? roomListItem.RoomId)
+ // .Replace('&', '_')
+ // .Replace(':', '_')
+ // .Replace('\'', '_')
+ // .Replace(' ', '_')
+ .ToList();
+ validFileNameChars.RemoveAll(Path.GetInvalidFileNameChars().Contains);
+ var filename = string.Join("", validFileNameChars);
+ while (File.Exists(filename))
+ filename += "_";
+
+ await File.WriteAllTextAsync(dirName + "/" + filename + ".json", rb.ToJson());
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) { }
+
+ private async Task PrintHelp() {
+ Console.WriteLine("Usage: new [filename] [options]");
+ Console.WriteLine("Options:");
+ Console.WriteLine(" --help Show this help message");
+ Console.WriteLine(" --version <version> Set the room version (e.g. 9, 10, 11, 12)");
+ Console.WriteLine("-- New room options --");
+ Console.WriteLine(" --alias <alias> Set the room alias (local part)");
+ Console.WriteLine(" --avatar-url <url> Set the room avatar URL");
+ Console.WriteLine(" --copy-avatar <roomId> Copy the avatar from an existing room");
+ Console.WriteLine(" --copy-powerlevels <roomId> Copy power levels from an existing room");
+ Console.WriteLine(" --invite-admin <userId> Invite a user as an admin (userId must start with '@')");
+ Console.WriteLine(" --invite <userId> Invite a user (userId must start with '@')");
+ Console.WriteLine(" --name <name> Set the room name (can be multiple words)");
+ Console.WriteLine(" --topic <topic> Set the room topic (can be multiple words)");
+ Console.WriteLine(" --federate <true|false> Set whether the room is federatable");
+ Console.WriteLine(" --public Set the room join rule to public");
+ Console.WriteLine(" --invite-only Set the room join rule to invite-only");
+ Console.WriteLine(" --knock Set the room join rule to knock");
+ Console.WriteLine(" --restricted Set the room join rule to restricted");
+ Console.WriteLine(" --knock_restricted Set the room join rule to knock_restricted");
+ Console.WriteLine(" --private Set the room join rule to private");
+ Console.WriteLine(" --join-rule <rule> Set the room join rule (public, invite, knock, restricted, knock_restricted, private)");
+ Console.WriteLine(" --history-visibility <visibility> Set the room history visibility (shared, invited, joined, world_readable)");
+ Console.WriteLine(" --type <type> Set the room type (e.g. m.space, m.room, support.feline.policy.list.msc.v1 etc.)");
+ // upgrade opts
+ Console.WriteLine("-- Upgrade options --");
+ Console.WriteLine(
+ " --upgrade <roomId> Create a room upgrade file instead of a new room file - WARNING: incompatible with non-upgrade options");
+ Console.WriteLine(" --invite-members Invite members during room upgrade");
+ Console.WriteLine(" --invite-powerlevel-users Invite users with power levels during room upgrade");
+ Console.WriteLine(" --migrate-bans Migrate bans during room upgrade");
+ Console.WriteLine(" --migrate-empty-state-events Migrate empty state events during room upgrade");
+ Console.WriteLine(" --upgrade-unstable-values Upgrade unstable values during room upgrade");
+ Console.WriteLine(" --msc4321-policy-list-upgrade <move|transition> Upgrade MSC4321 policy list");
+ Console.WriteLine(
+ "WARNING: The --upgrade option is incompatible with options listed under \"New room\", please use the equivalent options in the `modify` command instead.");
+ await host.StopAsync();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/Extensions/RoomBuilderExtensions.cs b/MatrixUtils.RoomUpgradeCLI/Extensions/RoomBuilderExtensions.cs
new file mode 100644
index 0000000..75852bc
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Extensions/RoomBuilderExtensions.cs
@@ -0,0 +1,252 @@
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+
+namespace MatrixUtils.RoomUpgradeCLI.Extensions;
+
+public static class RoomBuilderExtensions {
+ public static async Task ApplyRoomUpgradeCLIArgs(this RoomBuilder rb, AuthenticatedHomeserverGeneric hs, string[] args, bool isNewState = false) {
+ for (int i = 0; i < args.Length; i++) {
+ // Console.WriteLine($"Parsing arg {i}: {args[i]}");
+ switch (args[i]) {
+ case "--alias":
+ rb.AliasLocalPart = args[++i];
+ break;
+ case "--avatar-url":
+ rb.Avatar!.Url = args[++i];
+ break;
+ case "--copy-avatar": {
+ var room = hs.GetRoom(args[++i]);
+ rb.Avatar = await room.GetAvatarUrlAsync() ?? throw new ArgumentException($"Room {room.RoomId} does not have an avatar");
+ break;
+ }
+ case "--copy-powerlevels": {
+ var room = hs.GetRoom(args[++i]);
+ rb.PowerLevels = await room.GetPowerLevelsAsync() ?? throw new ArgumentException($"Room {room.RoomId} does not have power levels???");
+ break;
+ }
+ case "--invite-admin":
+ var inviteAdmin = args[++i];
+ if (!inviteAdmin.StartsWith('@')) {
+ throw new ArgumentException("Invalid user reference: " + inviteAdmin);
+ }
+
+ rb.Invites.Add(inviteAdmin, "Marked explicitly as admin to be invited");
+ break;
+ case "--invite":
+ var inviteUser = args[++i];
+ if (!inviteUser.StartsWith('@')) {
+ throw new ArgumentException("Invalid user reference: " + inviteUser);
+ }
+
+ rb.Invites.Add(inviteUser, "Marked explicitly to be invited");
+ break;
+ case "--name":
+ var nameEvt = rb.Name = new() { Name = "" };
+ while (i + 1 < args.Length && !args[i + 1].StartsWith("--")) {
+ nameEvt.Name += (nameEvt.Name.Length > 0 ? " " : "") + args[++i];
+ }
+
+ break;
+ case "--topic":
+ var topicEvt = rb.Topic = new() { Topic = "" };
+ while (i + 1 < args.Length && !args[i + 1].StartsWith("--")) {
+ topicEvt.Topic += (topicEvt.Topic.Length > 0 ? " " : "") + args[++i];
+ }
+
+ break;
+ case "--federate":
+ rb.IsFederatable = GetBoolArg(args, ref i, true);
+ break;
+ case "--public":
+ case "--invite-only":
+ case "--knock":
+ case "--restricted":
+ case "--knock_restricted":
+ case "--private":
+ rb.JoinRules.JoinRule = args[i].Replace("--", "").ToLowerInvariant() switch {
+ "public" => RoomJoinRulesEventContent.JoinRules.Public,
+ "invite-only" => RoomJoinRulesEventContent.JoinRules.Invite,
+ "knock" => RoomJoinRulesEventContent.JoinRules.Knock,
+ "restricted" => RoomJoinRulesEventContent.JoinRules.Restricted,
+ "knock_restricted" => RoomJoinRulesEventContent.JoinRules.KnockRestricted,
+ "private" => RoomJoinRulesEventContent.JoinRules.Private,
+ _ => throw new ArgumentException("Unknown join rule: " + args[i])
+ };
+ break;
+ case "--join-rule":
+ if (i + 1 >= args.Length || !args[i + 1].StartsWith("--")) {
+ throw new ArgumentException("Expected join rule after --join-rule");
+ }
+
+ rb.JoinRules.JoinRule = args[++i].ToLowerInvariant() switch {
+ "public" => RoomJoinRulesEventContent.JoinRules.Public,
+ "invite" => RoomJoinRulesEventContent.JoinRules.Invite,
+ "knock" => RoomJoinRulesEventContent.JoinRules.Knock,
+ "restricted" => RoomJoinRulesEventContent.JoinRules.Restricted,
+ "knock_restricted" => RoomJoinRulesEventContent.JoinRules.KnockRestricted,
+ "private" => RoomJoinRulesEventContent.JoinRules.Private,
+ _ => throw new ArgumentException("Unknown join rule: " + args[i])
+ };
+ break;
+ case "--history-visibility":
+ rb.HistoryVisibility = new RoomHistoryVisibilityEventContent {
+ HistoryVisibility = args[++i].ToLowerInvariant() switch {
+ "shared" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared,
+ "invited" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Invited,
+ "joined" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Joined,
+ "world_readable" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.WorldReadable,
+ _ => throw new ArgumentException("Unknown history visibility: " + args[i])
+ }
+ };
+ break;
+ case "--type":
+ rb.Type = args[++i];
+ break;
+ case "--version":
+ rb.Version = args[++i];
+ // if (!RoomBuilder.V12PlusRoomVersions.Contains(rb.Version)) {
+ // logger.LogWarning("Using room version {Version} which is not v12 or higher, this may cause issues with some features.", rb.Version);
+ // }
+ break;
+ case "--encryption":
+ if (args[i + 1].StartsWith("--")) {
+ rb.Encryption.Algorithm = "m.megolm.v1.aes-sha2";
+ }
+ else {
+ rb.Encryption.Algorithm = args[++i];
+ if (rb.Encryption.Algorithm == "null")
+ rb.Encryption.Algorithm = null; // disable encryption
+ }
+
+ break;
+ // upgrade options
+ case "--invite-members":
+ if (rb is not RoomUpgradeBuilder upgradeBuilder) {
+ throw new InvalidOperationException("Invite members can only be used with room upgrades");
+ }
+
+ upgradeBuilder.UpgradeOptions.InviteMembers = GetBoolArg(args, ref i, true);
+ break;
+ case "--invite-powerlevel-users":
+ case "--invite-power-level-users":
+ if (rb is not RoomUpgradeBuilder upgradeBuilderInvite) {
+ throw new InvalidOperationException("Invite powerlevel users can only be used with room upgrades");
+ }
+
+ upgradeBuilderInvite.UpgradeOptions.InvitePowerlevelUsers = GetBoolArg(args, ref i, true);
+ break;
+ case "--synapse-admin-join-local-users":
+ rb.SynapseAdminAutoAcceptLocalInvites = GetBoolArg(args, ref i, true);
+ break;
+ case "--migrate-bans":
+ if (rb is not RoomUpgradeBuilder upgradeBuilderBan) {
+ throw new InvalidOperationException("Migrate bans can only be used with room upgrades");
+ }
+
+ upgradeBuilderBan.UpgradeOptions.MigrateBans = GetBoolArg(args, ref i, true);
+ break;
+ case "--migrate-empty-state-events":
+ if (rb is not RoomUpgradeBuilder upgradeBuilderEmpty) {
+ throw new InvalidOperationException("Migrate empty state events can only be used with room upgrades");
+ }
+
+ upgradeBuilderEmpty.UpgradeOptions.MigrateEmptyStateEvents = GetBoolArg(args, ref i, true);
+ break;
+ case "--upgrade-unstable-values":
+ if (rb is not RoomUpgradeBuilder upgradeBuilderUnstable) {
+ throw new InvalidOperationException("Update unstable values can only be used with room upgrades");
+ }
+
+ upgradeBuilderUnstable.UpgradeOptions.UpgradeUnstableValues = GetBoolArg(args, ref i, true);
+ break;
+ case "--msc4321-policy-list-upgrade":
+ if (rb is not RoomUpgradeBuilder upgradeBuilderPolicy) {
+ throw new InvalidOperationException("MSC4321 policy list upgrade can only be used with room upgrades");
+ }
+
+ upgradeBuilderPolicy.UpgradeOptions.Msc4321PolicyListUpgradeOptions.Enable = true;
+ upgradeBuilderPolicy.UpgradeOptions.Msc4321PolicyListUpgradeOptions.UpgradeType = args[++i].ToLowerInvariant() switch {
+ "move" => RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Move,
+ "transition" => RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Transition,
+ _ => throw new ArgumentException("Unknown MSC4321 policy list upgrade type: " + args[i])
+ };
+ break;
+ case "--force-upgrade":
+ if (rb is not RoomUpgradeBuilder upgradeBuilderForce) {
+ throw new InvalidOperationException("Force upgrade can only be used with room upgrades");
+ }
+
+ upgradeBuilderForce.UpgradeOptions.ForceUpgrade = GetBoolArg(args, ref i, true);
+ break;
+ case "--noop-upgrade":
+ if (rb is not RoomUpgradeBuilder upgradeBuilderNoop) {
+ throw new InvalidOperationException("No-op upgrade can only be used with room upgrades");
+ }
+
+ upgradeBuilderNoop.UpgradeOptions.NoopUpgrade = GetBoolArg(args, ref i, true);
+ break;
+ case "--upgrade":
+ if (rb is not RoomUpgradeBuilder upgradeBuilderUpgrade) {
+ throw new InvalidOperationException("Upgrade can only be used with room upgrades");
+ }
+
+ if (isNewState) {
+ upgradeBuilderUpgrade.OldRoomId = args[++i];
+ Console.WriteLine($"Popping arg for --upgrade(isNewState={isNewState}): " + upgradeBuilderUpgrade.OldRoomId);
+ }
+
+ break;
+ case "--help":
+ PrintHelpAndExit();
+ return;
+ default:
+ throw new ArgumentException("Unknown argument: " + args[i]);
+ }
+ }
+ }
+
+ private static bool GetBoolArg(string[] args, ref int i, bool defaultValue) {
+ if (i + 1 < args.Length && bool.TryParse(args[i + 1], out var result)) {
+ i++;
+ return result;
+ }
+
+ return defaultValue;
+ }
+
+ private static void PrintHelpAndExit() {
+ Console.WriteLine("""
+ --help Show this help message
+ --version <version> Set the room version (e.g. 9, 10, 11, 12)
+ -- New room options --
+ --federate [True|false] Set whether the room is federatable [WARNING: Cannot be updated later!]
+ --type <type> Set the room type (e.g. m.space, m.room, support.feline.policy.list.msc.v1 etc.) [WARNING: Cannot be updated later!]
+ --alias <alias> Set the room alias (local part)
+ --avatar-url <url> Set the room avatar URL
+ --copy-avatar <roomId> Copy the avatar from an existing room
+ --copy-powerlevels <roomId> Copy power levels from an existing room
+ --invite <userId> Invite a user (userId must start with '@')
+ --invite-admin <userId> Invite a user as an admin (userId must start with '@')
+ --synapse-admin-join-local-users [True|false] Automatically accept local user invites during room creation (Synapse only, requires synapse admin access)
+ --name <name> Set the room name (can be multiple words)
+ --topic <topic> Set the room topic (can be multiple words)
+ --join-rule <rule> Set the room join rule (public, invite, knock, restricted, knock_restricted, private)
+ Aliases: --public, --invite, --knock, --restricted, --knock_restricted, --private
+ --history-visibility <visibility> Set the room history visibility (shared, invited, joined, world_readable)
+ -- Upgrade options --
+ --upgrade <roomId> Create a room upgrade file instead of a new room file - WARNING: incompatible with non-upgrade options
+ --invite-members [True|false] Invite members during room upgrade
+ --invite-local-users [True|false] Invite local users during room upgrade (also see --synapse-admin-join-local-users)
+ --invite-powerlevel-users [True|false] Invite users with power levels during room upgrade
+ --migrate-bans [True|false] Migrate bans during room upgrade
+ --migrate-empty-state-events [True|false] Migrate empty state events during room upgrade
+ --upgrade-unstable-values [True|false] Upgrade unstable values during room upgrade
+ --msc4321-policy-list-upgrade <move|transition> Upgrade MSC4321 policy list
+ --force-upgrade [True|false] Force upgrade even if you don't have the required permissions
+ --noop-upgrade [True|false] Perform the upgrade, but do not tombstone the old room
+ WARNING: The --upgrade option is incompatible with options listed under "New room", please use the equivalent options in the `modify` command instead.
+ """);
+ Environment.Exit(0);
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj b/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj
new file mode 100644
index 0000000..f349c81
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk.Worker">
+
+ <PropertyGroup>
+ <TargetFramework>net10.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UserSecretsId>dotnet-MatrixUtils.RoomUpgradeCLI-19ffcbc3-eeaa-4cef-b398-0db2008ca04b</UserSecretsId>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj"/>
+ <ProjectReference Include="..\LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="tmp\"/>
+ </ItemGroup>
+</Project>
diff --git a/MatrixUtils.RoomUpgradeCLI/Program.cs b/MatrixUtils.RoomUpgradeCLI/Program.cs
new file mode 100644
index 0000000..e169830
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Program.cs
@@ -0,0 +1,41 @@
+using ArcaneLibs.Extensions;
+using LibMatrix.Services;
+using LibMatrix.Utilities.Bot;
+using MatrixUtils.RoomUpgradeCLI;
+using MatrixUtils.RoomUpgradeCLI.Commands;
+
+foreach (var group in args.Split(";")) {
+ var argGroup = group.ToArray();
+ var builder = Host.CreateApplicationBuilder(args);
+ builder.Services.AddRoryLibMatrixServices();
+ builder.Services.AddMatrixBot();
+
+ if (argGroup.Length == 0) {
+ Console.WriteLine("Unknown command. Use 'new', 'modify', 'import-upgrade-state' or 'execute'.");
+ Console.WriteLine("Hint: you can chain commands with a semicolon (;) argument.");
+ return;
+ }
+
+ Console.WriteLine($"Running command: {string.Join(", ", argGroup)}");
+
+ builder.Services.AddSingleton(new RuntimeContext() {
+ Args = argGroup
+ });
+
+ if (argGroup[0] == "new") builder.Services.AddHostedService<NewFileCommand>();
+ else if (argGroup[0] == "new-from-room-dir") builder.Services.AddHostedService<NewFromRoomDirCommand>();
+ else if (argGroup[0] == "modify") builder.Services.AddHostedService<ModifyCommand>();
+ else if (argGroup[0] == "import-upgrade-state") builder.Services.AddHostedService<ImportUpgradeStateCommand>();
+ else if (argGroup[0] == "execute") builder.Services.AddHostedService<ExecuteCommand>();
+ // dev cmds
+ else if (argGroup[0] == "dev-delete-room") builder.Services.AddHostedService<DevDeleteRoomCommand>();
+ else if (argGroup[0] == "dev-delete-all-rooms") builder.Services.AddHostedService<DevDeleteAllRoomsCommand>();
+ else if (argGroup[0] == "dev-get-room-dir-state") builder.Services.AddHostedService<DevGetRoomDirStateCommand>();
+ else {
+ Console.WriteLine("Unknown command. Use 'new', 'modify', 'import-upgrade-state' or 'execute'.");
+ return;
+ }
+
+ var host = builder.Build();
+ host.Run();
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/Properties/launchSettings.json b/MatrixUtils.RoomUpgradeCLI/Properties/launchSettings.json
new file mode 100644
index 0000000..76f122f
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "MatrixUtils.RoomUpgradeCLI": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/MatrixUtils.RoomUpgradeCLI/RuntimeContext.cs b/MatrixUtils.RoomUpgradeCLI/RuntimeContext.cs
new file mode 100644
index 0000000..50e6781
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/RuntimeContext.cs
@@ -0,0 +1,5 @@
+namespace MatrixUtils.RoomUpgradeCLI;
+
+public class RuntimeContext {
+ public string[] Args { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json b/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json
new file mode 100644
index 0000000..621d281
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json
@@ -0,0 +1,17 @@
+{
+ // Don't touch this unless you know what you're doing:
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "LibMatrixBot": {
+ // Homeserver to connect to.
+ // Note: Homeserver resolution is applied here, but a direct base URL can be used.
+// "Homeserver": "rory.gay",
+
+ // Absolute path to the file containing the access token
+ "AccessTokenPath": "/home/Rory/matrix_access_token"
+ }
+}
diff --git a/MatrixUtils.RoomUpgradeCLI/appsettings.json b/MatrixUtils.RoomUpgradeCLI/appsettings.json
new file mode 100644
index 0000000..4feb15c
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Warning"
+ }
+ }
+}
diff --git a/MatrixUtils.RoomUpgradeCLI/mass-upgrade.sh b/MatrixUtils.RoomUpgradeCLI/mass-upgrade.sh
new file mode 100755
index 0000000..f21ea3c
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/mass-upgrade.sh
@@ -0,0 +1,9 @@
+#! /usr/bin/env sh
+dotnet build -c Release
+cat lst | while read id
+do
+ DOTNET_ENVIRONMENT=Local dotnet bin/Release/net9.0/MatrixUtils.RoomUpgradeCLI.dll new tmp/$id.json --upgrade $id --upgrade-unstable-values --force-upgrade --invite-powerlevel-users \; \
+ import-upgrade-state tmp/$id.json \; \
+ modify tmp/$id.json --version 12 &
+done
+wait
\ No newline at end of file
diff --git a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
index f2d47ea..880401a 100644
--- a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
+++ b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
@@ -1,22 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net10.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="10.0.0-rc.2.25502.107"/>
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\MatrixUtils.Web\MatrixUtils.Web.csproj" />
+ <ProjectReference Include="..\MatrixUtils.Web\MatrixUtils.Web.csproj"/>
</ItemGroup>
<ItemGroup>
- <Folder Include="Controllers" />
+ <Folder Include="Controllers"/>
</ItemGroup>
diff --git a/MatrixUtils.Web.Server/Program.cs b/MatrixUtils.Web.Server/Program.cs
index cad3878..59d450a 100644
--- a/MatrixUtils.Web.Server/Program.cs
+++ b/MatrixUtils.Web.Server/Program.cs
@@ -1,3 +1,6 @@
+using LibMatrix.Services;
+using MatrixUtils.Web.Classes;
+
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
@@ -5,6 +8,9 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
+builder.Services.AddRoryLibMatrixServices();
+builder.Services.AddScoped<RmuSessionStore>();
+
var app = builder.Build();
// Configure the HTTP request pipeline.
diff --git a/MatrixUtils.Web/App.razor b/MatrixUtils.Web/App.razor
index 5e87bc3..7e8e1c3 100644
--- a/MatrixUtils.Web/App.razor
+++ b/MatrixUtils.Web/App.razor
@@ -1,4 +1,4 @@
-@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
@@ -11,3 +11,9 @@
</LayoutView>
</NotFound>
</Router>
+
+@code {
+
+ public static WebAssemblyHost Host { get; set; } = null!;
+
+}
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..ddf3eed 100644
--- a/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
+++ b/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
@@ -3,26 +3,20 @@ using LibMatrix.Interfaces.Services;
namespace MatrixUtils.Web.Classes;
-public class LocalStorageProviderService : IStorageProvider {
- private readonly ILocalStorageService _localStorageService;
-
- public LocalStorageProviderService(ILocalStorageService localStorageService) {
- _localStorageService = localStorageService;
- }
-
+public class LocalStorageProviderService(ILocalStorageService localStorageService) : IStorageProvider {
Task IStorageProvider.SaveAllChildrenAsync<T>(string key, T value) {
throw new NotImplementedException();
}
Task<T?> IStorageProvider.LoadAllChildrenAsync<T>(string key) where T : default => throw new NotImplementedException();
- async Task IStorageProvider.SaveObjectAsync<T>(string key, T value) => await _localStorageService.SetItemAsync(key, value);
+ async Task IStorageProvider.SaveObjectAsync<T>(string key, T value) => await localStorageService.SetItemAsync(key, value);
- async Task<T?> IStorageProvider.LoadObjectAsync<T>(string key) where T : default => await _localStorageService.GetItemAsync<T>(key);
+ async Task<T?> IStorageProvider.LoadObjectAsync<T>(string key) where T : default => await localStorageService.GetItemAsync<T>(key);
- async Task<bool> IStorageProvider.ObjectExistsAsync(string key) => await _localStorageService.ContainKeyAsync(key);
+ 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);
+ 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..1611b83
--- /dev/null
+++ b/MatrixUtils.Web/Classes/RmuSessionStore.cs
@@ -0,0 +1,336 @@
+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 (string.IsNullOrEmpty(sessionId)) {
+ logger.LogWarning("No session ID provided.");
+ return null;
+ }
+
+ 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");
+ if (currentSessionId == null) {
+ if (log) logger.LogWarning("No current session ID found in storage.");
+ return null;
+ }
+
+ 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
+ };
+
+ await SaveStorage();
+ if (CurrentSession == null) await SetCurrentSession(sessionId);
+
+ return sessionId;
+ }
+
+ public async Task RemoveSession(string sessionId) {
+ await LoadStorage();
+ if (SessionCache.Count == 0) {
+ logger.LogWarning("No sessions found.");
+ return;
+ }
+
+ logger.LogTrace("Removing session {sessionId}.", sessionId);
+
+ if ((await GetCurrentSession())?.SessionId == sessionId)
+ await SetCurrentSession(SessionCache.FirstOrDefault(x => x.Key != sessionId).Key);
+
+ if (SessionCache.Remove(sessionId)) {
+ logger.LogInformation("RemoveSession: Removed session {sessionId}.", sessionId);
+ logger.LogInformation("RemoveSession: Remaining sessions: {sessionIds}.", string.Join(", ", SessionCache.Keys));
+ await SaveStorage(log: true);
+ }
+ else
+ logger.LogWarning("RemoveSession: Session {sessionId} not found.", sessionId);
+ }
+
+ 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;
+ }
+ }
+
+ public async IAsyncEnumerable<AuthenticatedHomeserverGeneric> TryGetAllHomeservers(bool log = true, bool ignoreFailures = true) {
+ await LoadStorage();
+ if (log) logger.LogTrace("Getting all homeservers.");
+ var tasks = SessionCache.Values.Select(async session => {
+ if (ignoreFailures && session.Auth.LastFailureReason != null && session.Auth.LastFailureReason != UserAuth.FailureReason.None) {
+ if (log) logger.LogTrace("Skipping session {sessionId} due to previous failure: {reason}", session.SessionId, session.Auth.LastFailureReason);
+ return null;
+ }
+
+ try {
+ var hs = await GetHomeserver(session.SessionId, log: false);
+ if (session.Auth.LastFailureReason != null) {
+ SessionCache[session.SessionId].Auth.LastFailureReason = null;
+ await SaveStorage();
+ }
+
+ return hs;
+ }
+ catch (Exception e) {
+ logger.LogError("TryGetAllHomeservers: Failed to get homeserver for {userId} via {homeserver}: {ex}", session.Auth.UserId, session.Auth.Homeserver, e);
+ var reason = SessionCache[session.SessionId].Auth.LastFailureReason = e switch {
+ MatrixException { ErrorCode: MatrixException.ErrorCodes.M_UNKNOWN_TOKEN } => UserAuth.FailureReason.InvalidToken,
+ HttpRequestException => UserAuth.FailureReason.NetworkError,
+ _ => UserAuth.FailureReason.UnknownError
+ };
+ await SaveStorage(log: true);
+
+ // await LoadStorage(true);
+ if (SessionCache[session.SessionId].Auth.LastFailureReason != reason) {
+ await Console.Error.WriteLineAsync(
+ $"Warning: Session {session.SessionId} failure reason changed during reload from {reason} to {SessionCache[session.SessionId].Auth.LastFailureReason}");
+ }
+
+ throw;
+ }
+ }).ToList();
+
+ while (tasks.Count != 0) {
+ var finished = await Task.WhenAny(tasks);
+ tasks.Remove(finished);
+ if (finished.IsFaulted) continue;
+
+ var result = await finished;
+ if (result != null) yield return result;
+ }
+ }
+
+#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(bool log = false) {
+ if (log) logger.LogWarning("Saving {count} sessions to storage.", SessionCache.Count);
+ await storageService.DataStorageProvider!.SaveObjectAsync("rmu.sessions",
+ SessionCache.ToDictionary(
+ x => x.Key,
+ x => x.Value.Auth
+ )
+ );
+ await storageService.DataStorageProvider.SaveObjectAsync("rmu.session", CurrentSession?.SessionId);
+ if (log) logger.LogWarning("{count} sessions saved to storage.", SessionCache.Count);
+ }
+
+#endregion
+
+#region Migrations
+
+ public async Task RunMigrations() {
+ await MigrateFromMru();
+ await MigrateAccountsToKeyedStorage();
+ }
+
+ private async Task MigrateFromMru() {
+ var dsp = storageService.DataStorageProvider!;
+ if (await dsp.ObjectExistsAsync("token") || await dsp.ObjectExistsAsync("tokens")) {
+ logger.LogInformation("Migrating from unnamespaced localstorage!");
+ if (await dsp.ObjectExistsAsync("token")) {
+ var oldToken = await dsp.LoadObjectAsync<UserAuth>("token");
+ if (oldToken != null) {
+ await dsp.SaveObjectAsync("mru.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("mru.tokens", oldTokens);
+ await dsp.DeleteObjectAsync("tokens");
+ }
+ }
+ }
+
+ if (await dsp.ObjectExistsAsync("mru.token") || await dsp.ObjectExistsAsync("mru.tokens")) {
+ logger.LogInformation("Migrating from MRU token namespace!");
+ if (await dsp.ObjectExistsAsync("mru.token")) {
+ var oldToken = await dsp.LoadObjectAsync<UserAuth>("mru.token");
+ if (oldToken != null) {
+ await dsp.SaveObjectAsync("rmu.token", oldToken);
+ await dsp.DeleteObjectAsync("mru.token");
+ }
+ }
+
+ 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..215ad14 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() {
@@ -83,10 +82,8 @@ public class DefaultRoomCreationTemplate : IRoomCreationTemplate {
//TODO: re-implement this
}
},
- CreationContent = new JsonObject {
- {
- "type", null
- }
+ CreationContent = new() {
+ { "type", null }
}
};
-}
+}
\ No newline at end of file
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/Classes/UserAuth.cs b/MatrixUtils.Web/Classes/UserAuth.cs
index 66476ae..16bb758 100644
--- a/MatrixUtils.Web/Classes/UserAuth.cs
+++ b/MatrixUtils.Web/Classes/UserAuth.cs
@@ -1,9 +1,11 @@
+using System.Text.Json.Serialization;
using LibMatrix.Responses;
namespace MatrixUtils.Web.Classes;
public class UserAuth : LoginResponse {
public UserAuth() { }
+
public UserAuth(LoginResponse login) {
Homeserver = login.Homeserver;
UserId = login.UserId;
@@ -12,4 +14,14 @@ public class UserAuth : LoginResponse {
}
public string? Proxy { get; set; }
-}
+
+ public FailureReason? LastFailureReason { get; set; }
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum FailureReason {
+ None,
+ InvalidToken,
+ NetworkError,
+ UnknownError
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj
index 8760e7a..f7ebb62 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>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LinkIncremental>true</LinkIncremental>
@@ -12,62 +12,68 @@
<BlazorEnableCompression>false</BlazorEnableCompression>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
<BlazorCacheBootResources>false</BlazorCacheBootResources>
-<!-- <RunAOTCompilation>true</RunAOTCompilation>-->
+ <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
+ <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
+ <WasmEnableHotReload>false</WasmEnableHotReload>
</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="10.0.0-rc.2.25502.107"/>
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0-rc.2.25502.107" PrivateAssets="all"/>
+ <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.0-rc.2.25502.107"/>
+ <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="10.0.0-rc.2.25502.107"/>
+ <PackageReference Include="SpawnDev.BlazorJS" Version="2.38.0"/>
+ <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.21.0"/>
</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..281cf07 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,11 +39,15 @@
@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");
-
- await base.OnInitializedAsync();
+ await (Task)typeof(RmuSessionStore).GetMethod("LoadStorage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.Invoke(sessionStore, [true])!;
+ await foreach (var _ in sessionStore.TryGetAllHomeservers()) { }
+
+ await (Task)typeof(RmuSessionStore).GetMethod("SaveStorage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.Invoke(sessionStore, [true])!;
}
private async Task LogStuff() {
@@ -55,8 +62,9 @@
foreach (var key in keys) {
data.Add(key, await TieredStorage.DataStorageProvider.LoadObjectAsync<object>(key));
}
+
var dataUri = "data:application/json;base64,";
- dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)));
+ dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)));
await JSRuntime.InvokeVoidAsync("window.open", dataUri, "_blank");
}
@@ -66,6 +74,7 @@
foreach (var (key, value) in data) {
await TieredStorage.DataStorageProvider.SaveObjectAsync(key, value);
}
+
NavigationManager.NavigateTo(NavigationManager.Uri, true, true);
}
diff --git a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
index bf5a396..f6392a4 100644
--- a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
+++ b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
@@ -1,9 +1,13 @@
@page "/Dev/Utilities"
@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.Ephemeral
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Helpers
@using MatrixUtils.Abstractions
<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> *@
@@ -13,7 +17,7 @@ else {
<summary>Room List</summary>
@foreach (var roomId in Rooms) {
<a style="color: unset; text-decoration: unset;" href="/RoomStateViewer/@roomId.Replace('.', '~')">
- <RoomListItem RoomInfo="@(new RoomInfo(hs.GetRoom(roomId)))" LoadData="true"></RoomListItem>
+ <RoomListItem Homeserver="hs" RoomInfo="@(new RoomInfo(hs.GetRoom(roomId)))" LoadData="true"></RoomListItem>
</a>
}
</details>
@@ -38,7 +42,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!");
@@ -60,6 +64,7 @@ else {
StateHasChanged();
return;
}
+
if (res.Content.Headers.ContentType.MediaType == "application/json")
GetRequestResult = $"Error: {res.StatusCode}\n" + (await res.Content.ReadFromJsonAsync<object>()).ToJson();
else
@@ -68,7 +73,32 @@ else {
catch (Exception e) {
GetRequestResult = $"Error: {e}";
}
+
StateHasChanged();
}
+ private async Task TestRoomBuilder() {
+ var rb = new RoomBuilder() {
+ HistoryVisibility = new RoomHistoryVisibilityEventContent() { HistoryVisibility = RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared },
+ ImportantState = [
+ new() {
+ RawContent = new() {
+ ["type"] = "m.room.name",
+ ["name"] = "Test Room"
+ }
+ },
+ new() {
+ Type = "test",
+ TypedContent = new PresenceEventContent() {
+ Presence = "online",
+ LastActiveAgo = 0,
+ }
+ },
+
+ ]
+ };
+
+ await rb.Create(hs);
+ }
+
}
\ No newline at end of file
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..c636c56
--- /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 OnClickAsync="@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..21b0972 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
@@ -1,5 +1,6 @@
@page "/HSAdmin"
@using ArcaneLibs.Extensions
+@using LibMatrix.Responses.Federation
<h3>Homeserver Admininistration</h3>
<hr/>
@@ -10,7 +11,15 @@ 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/UserQuery">Query users</a><br/>
+ <a href="/HSAdmin/Synapse/BlockMedia">Block media</a><br/>
+ <a href="/HSAdmin/Synapse/BackgroundJobs">View running background jobs</a><br/>
+ }
+ 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 +33,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..ec2ec54
--- /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 OnClickAsync="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..e9d0cd2
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
@@ -0,0 +1,192 @@
+@page "/HSAdmin/Synapse/BlockMedia"
+@using System.Text.Json
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec
+@using LibMatrix.StructuredData
+
+<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 OnClickAsync="@RedactAllEvents">Redact all messages</LinkButton>
+ }
+
+ @if (Event?.Sender?.Split(':', 2)[1] == Homeserver?.ServerName) {
+ <p>User is a local user!</p>
+ <LinkButton OnClickAsync="@DeactivateUser">Deactivate User</LinkButton>
+ <LinkButton OnClickAsync="@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/RoomQuery/SynapseRoomQueryFilter.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor
new file mode 100644
index 0000000..f1c5907
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor
@@ -0,0 +1,74 @@
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters
+@using MatrixUtils.Web.Shared.FilterComponents
+<div style="margin-left: 8px; margin-bottom: 8px;">
+ <u style="display: block;">String contains</u>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.RoomId" Label="Room ID"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Name" Label="Room name"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.CanonicalAlias" Label="Canonical alias"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Creator" Label="Creator"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Version" Label="Room version"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Encryption" Label="Encryption algorithm"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.JoinRules" Label="Join rules"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.GuestAccess" Label="Guest access"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.HistoryVisibility" Label="History visibility"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Topic" Label="Topic"/></span>
+
+ <u style="display: block;">Optional checks</u>
+ <span class="tile tile150"><BooleanFilterComponent Filter="@Filter.Federation" Label="Is federated"/></span>
+ <span class="tile tile150"><BooleanFilterComponent Filter="@Filter.Public" Label="Is public"/></span>
+ <span class="tile tile150"><BooleanFilterComponent Filter="@Filter.Tombstone" Label="Is tombstoned"/></span>
+
+ <u style="display: block;">Ranges</u>
+ <span class="tile center-children">
+ <InputCheckbox @bind-Value="@Filter.StateEvents.Enabled"/>
+ @if (!Filter.StateEvents.Enabled) {
+ <span>State events</span>
+ }
+ else {
+ <InputCheckbox @bind-Value="@Filter.StateEvents.CheckGreaterThan"/>
+ <span> </span>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEvents.GreaterThan"/>
+ <span class="range-sep">state events</span>
+ <InputCheckbox @bind-Value="@Filter.StateEvents.CheckLessThan"/>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEvents.LessThan"/>
+ }
+ </span>
+ <span class="tile center-children">
+ <InputCheckbox @bind-Value="@Filter.JoinedMembers.Enabled"/>
+ @if (!Filter.JoinedMembers.Enabled) {
+ <span>Joined members</span>
+ }
+ else {
+ <InputCheckbox @bind-Value="@Filter.JoinedMembers.CheckGreaterThan"/>
+ <span> </span>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembers.GreaterThan"/>
+ <span class="range-sep">members</span>
+ <InputCheckbox @bind-Value="@Filter.JoinedMembers.CheckLessThan"/>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembers.LessThan"/>
+ }
+ </span>
+ <span class="tile center-children">
+ <InputCheckbox @bind-Value="@Filter.JoinedLocalMembers.Enabled"/>
+ <span> </span>
+ @if (!Filter.JoinedLocalMembers.Enabled) {
+ <span>Joined local members</span>
+ }
+ else {
+ <InputCheckbox @bind-Value="@Filter.JoinedLocalMembers.CheckGreaterThan"/>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembers.GreaterThan"/>
+ <span class="range-sep">local members</span>
+ <InputCheckbox @bind-Value="@Filter.JoinedLocalMembers.CheckLessThan"/>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembers.LessThan"/>
+ }
+ </span>
+</div>
+@* @{ *@
+@* Console.WriteLine($"Rendered SynapseRoomQueryFilter with filter: {Filter.ToJson()}"); *@
+@* } *@
+
+@code {
+
+ [Parameter]
+ public required SynapseAdminLocalRoomQueryFilter Filter { get; set; }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css
new file mode 100644
index 0000000..83ce426
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css
@@ -0,0 +1,35 @@
+.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;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor
new file mode 100644
index 0000000..5591072
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor
@@ -0,0 +1,5 @@
+<h3>SynapseRoomQueryResult</h3>
+
+@code {
+
+}
\ No newline at end of file
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..fc9f8e8
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -0,0 +1,266 @@
+@using System.Text.Json.Serialization
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Homeservers.Extensions.NamedCaches
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses
+
+@if (string.IsNullOrWhiteSpace(Context.DeleteId) || EditorOnly) {
+ <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/>
+ <details>
+ <summary>Media</summary>
+ <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"/>
+ </details>
+
+ <details>
+ <summary>Local users</summary>
+ <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/>
+ <span>Follow tombstone (if any): </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.FollowTombstone"/>
+ @if (!EditorOnly) {
+ <LinkButton InlineText="true" OnClickAsync="@FollowTombstoneAsync">Exec</LinkButton>
+ }
+ </details>
+
+ <details>
+ <summary>Issue warning to local members (optional)</summary>
+ <b>All fields are required if used!</b><br/>
+ <span>Warning room User ID: </span>
+ <FancyTextBox @bind-Value="@Context.DeleteRequest.NewRoomUserId"/>
+ <br/>
+ <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/>
+ </details>
+
+ @if (!EditorOnly) {
+ <LinkButton OnClickAsync="@DeleteRoom">Execute</LinkButton>
+ }
+}
+else {
+ <pre>
+ @(_status?.ToJson() ?? "Loading status...")
+ </pre>
+ <br/>
+ <LinkButton InlineText="true" OnClickAsync="@OnComplete">[Stop tracking]</LinkButton>
+ if (_status?.Status == SynapseAdminRoomDeleteStatus.Failed) {
+ <LinkButton InlineText="true" OnClickAsync="@ForceDelete">[Force delete]</LinkButton>
+ }
+}
+
+@code {
+
+ [Parameter]
+ public required RoomShutdownContext Context { get; set; }
+
+ [Parameter]
+ public required AuthenticatedHomeserverSynapse Homeserver { get; set; }
+
+ [Parameter]
+ public bool EditorOnly { get; set; }
+
+ private NamedCache<RoomShutdownContext> TaskMap { get; set; } = null!;
+ private SynapseAdminRoomDeleteStatus? _status = null;
+ private bool _isTracking = false;
+
+ protected override async Task OnInitializedAsync() {
+ if (EditorOnly) return;
+ TaskMap = new NamedCache<RoomShutdownContext>(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks");
+ var existing = await TaskMap.GetValueAsync(Context.RoomId);
+ if (existing is not null) {
+ Context = existing;
+ }
+
+ if (Context.ExecuteImmediately)
+ await DeleteRoom();
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender) {
+ if (EditorOnly) return;
+ if (!_isTracking) {
+ if (!string.IsNullOrWhiteSpace(Context.DeleteId)) {
+ _isTracking = true;
+ _ = Task.Run(async () => {
+ do {
+ _status = await Homeserver.Admin.GetRoomDeleteStatus(Context.DeleteId);
+ StateHasChanged();
+ if (_status.Status == SynapseAdminRoomDeleteStatus.Complete) {
+ await OnComplete();
+ break;
+ }
+
+ await Task.Delay(1000);
+ } while (_status.Status != SynapseAdminRoomDeleteStatus.Failed && _status.Status != SynapseAdminRoomDeleteStatus.Complete);
+ });
+ }
+ }
+ }
+
+ public class RoomShutdownContext {
+ public required string RoomId { get; set; }
+
+ [JsonIgnore] // do NOT persist - this triggers immediate purging
+ public bool ExecuteImmediately { 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 SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom? RoomDetails { get; set; }
+
+ public class ExtraDeleteOptions {
+ public bool FollowTombstone { get; set; }
+
+ // media 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() {
+ if (EditorOnly) return;
+ Console.WriteLine($"Room shutdown task for {Context.RoomId} completed, removing from map.");
+ await OnCompleteLock.WaitAsync();
+ try {
+ await TaskMap.RemoveValueAsync(Context.RoomId!);
+ }
+ catch (Exception e) {
+ Console.WriteLine("Failed to remove completed room shutdown task from map: " + e);
+ }
+ finally {
+ OnCompleteLock.Release();
+ }
+ }
+
+ public async Task DeleteRoom() {
+ if (EditorOnly) return;
+ if (Context.ExtraOptions.FollowTombstone) await FollowTombstoneAsync();
+
+ Console.WriteLine($"Deleting room {Context.RoomId} with options: " + Context.DeleteRequest.ToJson());
+
+ var resp = await Homeserver.Admin.DeleteRoom(Context.RoomId, Context.DeleteRequest, false);
+ Context.DeleteId = resp.DeleteId;
+ await TaskMap.SetValueAsync(Context.RoomId, Context);
+ }
+
+ private static readonly SemaphoreSlim OnCompleteLock = new(1, 1);
+
+ private async Task FollowTombstoneAsync() {
+ if (EditorOnly) return;
+ var tomb = await TryGetTombstoneAsync();
+ var content = tomb?.ContentAs<RoomTombstoneEventContent>();
+ if (content != null && !string.IsNullOrWhiteSpace(content.ReplacementRoom)) {
+ Console.WriteLine("Tombstone: " + tomb.ToJson());
+ if (!content.ReplacementRoom.StartsWith('!')) {
+ Console.WriteLine($"Invalid replacement room ID in tombstone: {content.ReplacementRoom}, ignoring!");
+ }
+ else {
+ var oldMembers = await Homeserver.Admin.GetRoomMembersAsync(Context.RoomId, localOnly: true);
+ var isKnownRoom = await Homeserver.Admin.CheckRoomKnownAsync(content.ReplacementRoom);
+ var targetMembers = isKnownRoom
+ ? await Homeserver.Admin.GetRoomMembersAsync(Context.RoomId, localOnly: true)
+ : new() { Members = [] };
+
+ var members = oldMembers.Members.Except(targetMembers.Members).ToList();
+ Console.WriteLine("To migrate: " + members.ToJson());
+ foreach (var member in members) {
+ var success = false;
+ do {
+ var sess = member == Homeserver.WhoAmI.UserId ? Homeserver : await Homeserver.Admin.GetHomeserverForUserAsync(member, TimeSpan.FromSeconds(15));
+ var oldRoom = sess.GetRoom(Context.RoomId);
+ var room = sess.GetRoom(content.ReplacementRoom);
+ try {
+ var servers = (await oldRoom.GetMembersByHomeserverAsync(joinedOnly: true))
+ .Select(x => new KeyValuePair<string, int>(x.Key, x.Value.Count))
+ .OrderByDescending(x => x.Key == "matrix.org" ? 0 : x.Value); // try anything else first, to reduce load on matrix.org
+
+ await room.JoinAsync(servers.Take(10).Select(x => x.Key).ToArray(), reason: "Automatically following tombstone as old room is being purged.", checkIfAlreadyMember: isKnownRoom);
+ Console.WriteLine($"Migrated {member} from {Context.RoomId} to {content.ReplacementRoom}");
+ success = true;
+ }
+ catch (Exception e) {
+ if (e is MatrixException { ErrorCode: "M_FORBIDDEN" }) {
+ Console.WriteLine($"Cannot migrate {member} to {content.ReplacementRoom}: {(e as MatrixException)!.GetAsJson()}");
+ success = true; // give up
+ continue;
+ }
+
+ Console.WriteLine($"Failed to invite {member} to {content.ReplacementRoom}: {e}");
+ success = false;
+ await Task.Delay(1000);
+ }
+ } while (!success);
+ }
+ }
+ }
+ }
+
+ private async Task<StateEventResponse?> TryGetTombstoneAsync() {
+ if (EditorOnly) return null;
+ try {
+ return (await Homeserver.Admin.GetRoomStateAsync(Context.RoomId, RoomTombstoneEventContent.EventId)).Events.FirstOrDefault(x => x.StateKey == "");
+ }
+ catch {
+ return null;
+ }
+ }
+
+ private async Task ForceDelete() {
+ if (EditorOnly) return;
+ Console.WriteLine($"Forcing purge for {Context.RoomId}!");
+ await OnCompleteLock.WaitAsync();
+ try {
+ var resp = await Homeserver.Admin.DeleteRoom(Context.RoomId, new() {
+ ForcePurge = true
+ }, waitForCompletion: false);
+ Context.DeleteId = resp.DeleteId;
+ await TaskMap.SetValueAsync(Context.RoomId, Context);
+ StateHasChanged();
+ }
+ catch (Exception e) {
+ Console.WriteLine("Failed to remove completed room shutdown task from map: " + e);
+ }
+ finally {
+ OnCompleteLock.Release();
+ }
+ }
+
+}
\ 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..05899c8
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -0,0 +1,618 @@
+@page "/HSAdmin/Synapse/RoomQuery"
+@using System.Diagnostics.CodeAnalysis
+@using System.Text.Json
+@using ArcaneLibs.Blazor.Components.Services
+@using Microsoft.AspNetCore.WebUtilities
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Homeservers.Extensions.NamedCaches
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses
+@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components
+@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components.RoomQuery
+@inject ILogger<RoomQuery> Logger
+@inject BlazorSaveFileService BlazorSaveFileService
+
+<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/>
+<InputCheckbox @bind-Value="Ascending"/>
+<label> Ascending</label><br/>
+<InputCheckbox @bind-Value="FetchV12PlusCreatorServer"/>
+<label> Fetch v12+ room creation homeserver</label>
+<LinkButton InlineText="true" OnClickAsync="FetchV12PlusCreatorServersAsync"> (Execute manually)</LinkButton><br/>
+<InputCheckbox @bind-Value="FetchTombstones"/>
+<label> Check for tombstone events</label>
+<LinkButton InlineText="true" OnClickAsync="FetchTombstoneEventsAsync"> (Execute manually)</LinkButton><br/>
+<InputCheckbox @bind-Value="SummarizeLocalMembers"/>
+<label> Fetch local member list for small rooms</label>
+<LinkButton InlineText="true" OnClickAsync="FetchLocalMemberEventsAsync"> (Execute manually)</LinkButton><br/>
+<InputCheckbox @bind-Value="ShowFullResultData"/>
+<label> Show full result data (JSON)</label><br/>
+<InputCheckbox @bind-Value="EnableMultiPurge"/>
+<label> Enable multi-purge mode</label>
+@if (EnableMultiPurge) {
+ <span> </span>
+ <LinkButton InlineText="true" OnClick="@MultiPurgeInvertSelection">[Invert selection]</LinkButton>
+ <span> </span>
+ <details style="display: inline-block;">
+ <summary>Edit purge options</summary>
+ <SynapseRoomShutdownWindowContent Context="@DefaultShutdownContext" Homeserver="Homeserver" EditorOnly="true"/>
+ </details>
+}
+else {
+ <br/>
+}
+<details>
+ <summary>Local filtering (slow)</summary>
+ <SynapseRoomQueryFilter Filter="@Filter"/>
+</details>
+<LinkButton OnClickAsync="@Search">Search</LinkButton>
+
+@if (EnableMultiPurge) {
+ <LinkButton Color="#FF8800" OnClick="@PurgeSelection">Purge selected rooms</LinkButton>
+}
+<br/>
+
+@if (Results.Count > 0) {
+ <p>Found @Results.Count rooms</p>
+}
+
+@foreach (var room in Results) {
+ <div class="room-list-item">
+ @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@
+ <p>
+ @if (EnableMultiPurge) {
+ <InputCheckbox @bind-Value="@room.MultiPurgeSelected"/>
+ <span> </span>
+ }
+ @if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) {
+ <span>@room.CanonicalAlias - </span>
+ }
+ <span>@room.RoomId</span>
+ @if (!string.IsNullOrWhiteSpace(room.Name)) {
+ <span> (@room.Name)</span>
+ }
+ <br/>
+
+ @if (!string.IsNullOrWhiteSpace(room.Creator)) {
+ <span>Created by @room.Creator</span>
+ <br/>
+ }
+ </p>
+ <p>
+ <LinkButton OnClickAsync="@(() => DeleteRoom(room))">Delete room</LinkButton>
+ <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={room.RoomId}&via={room.OriginHomeserver}")">Resync state</LinkButton>
+ <LinkButton OnClickAsync="@(() => ExportState(room))">@(room.JoinedLocalMembers == 0 ? "Try to export state" : "Export state")</LinkButton>
+ <LinkButton OnClickAsync="@(() => ForceJoin(room))">Force Join</LinkButton>
+ </p>
+
+ @{
+ List<string?> flags = [];
+ if (room.JoinedLocalMembers > 0) {
+ flags.Add(room.JoinRules switch {
+ "public" => "Public",
+ "invite" => "Invite only",
+ "knock" => "Knock",
+ "restricted" => "Restricted",
+ "knock_restricted" => "Knock + restricted",
+ // TODO: default?
+ null => null,
+ "" => null,
+ _ => "unknown join rule: " + room.JoinRules
+ });
+
+ if (!string.IsNullOrWhiteSpace(room.Encryption)) flags.Add("encrypted");
+ if (!room.Federatable) flags.Add("unfederated");
+
+ flags.Add(room.HistoryVisibility switch {
+ "world_readable" => "world readable history",
+ "shared" => "shared history",
+ "invited" => "history since invite",
+ "joined" => "history since join",
+ // TODO: default?
+ null => null,
+ "" => null,
+ _ => "unknown history setting: " + room.HistoryVisibility
+ });
+
+ flags.Add(room.GuestAccess switch {
+ "can_join" => "guests allowed",
+ "forbidden" => null,
+ // TODO: default?
+ null => null,
+ "" => null,
+ _ => "unknown guest access: " + room.GuestAccess,
+ });
+
+ flags = flags.Where(x => x != null).ToList();
+ }
+ }
+ <span>@string.Join(", ", flags)</span>
+ @if (room.JoinedLocalMembers == 0 && flags.Count > 0) {
+ <span> at the time of leaving</span>
+ }
+ <br/>
+
+ <span>@room.StateEvents state events, room version @(room.Version ?? "1")</span><br/>
+ @if (room.TombstoneEvent is not null) {
+ var tombstoneContent = room.TombstoneEvent.ContentAs<RoomTombstoneEventContent>()!;
+ <span>Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body</span>
+ <br/>
+ }
+
+ @{
+ var memberSummary = room.MemberSummary;
+ if (room.LocalMembers is not null) {
+ memberSummary += $": {string.Join(", ", room.LocalMembers)}";
+ }
+ }
+ <span>@memberSummary</span><br/>
+ @if (!string.IsNullOrWhiteSpace(room.TopicEvent?.ContentAs<RoomTopicEventContent>()?.Topic)) {
+ <details>
+ <summary>Room topic</summary>
+ <pre>@(room.TopicEvent?.ContentAs<RoomTopicEventContent>()?.Topic)</pre>
+ </details>
+ }
+ @foreach (var ex in room.Exceptions) {
+ <span style="color: red;">@ex</span>
+ <br/>
+ }
+ @if (ShowFullResultData) {
+ <details>
+ <summary>Full result data</summary>
+ <pre>@room.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) {
+ <ModalWindow Title="@($"Delete room {roomId}")" OnCloseClicked="@(() => {
+ DeleteRequests.Remove(roomId);
+ StateHasChanged();
+ })">
+ <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/>
+ </ModalWindow>
+}
+
+@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; } = true;
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "FetchV12PlusCreatorServer")]
+ public bool FetchV12PlusCreatorServer { get; set; } = true;
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "SummarizeLocalMembers")]
+ public bool SummarizeLocalMembers { get; set; } = true;
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "FetchTombstones")]
+ public bool FetchTombstones { get; set; } = true;
+
+ private List<RoomInfo> Results { get; set; } = new();
+
+ private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!;
+
+ private SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new();
+
+ private Dictionary<string, SynapseRoomShutdownWindowContent.RoomShutdownContext> DeleteRequests { get; set; } = [];
+
+ // private Dictionary<string, SynapseAdminRoomDeleteStatus> DeleteStatuses { get; set; } = new();
+
+ private NamedCache<SynapseRoomShutdownWindowContent.RoomShutdownContext> TaskMap { get; set; } = null!;
+
+ private SynapseRoomShutdownWindowContent.RoomShutdownContext DefaultShutdownContext { get; set; } = new() {
+ RoomId = "",
+ DeleteRequest = new() { Block = true, Purge = true, ForcePurge = false }
+ };
+
+ public bool ShowFullResultData {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ public bool EnableMultiPurge { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is not AuthenticatedHomeserverSynapse synapse) {
+ NavigationManager.NavigateTo("/");
+ return;
+ }
+
+ Homeserver = synapse;
+ TaskMap = new NamedCache<SynapseRoomShutdownWindowContent.RoomShutdownContext>(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks");
+ DeleteRequests = (await TaskMap.ReadCacheMapAsync()).Where(x => x.Value.DeleteId is not null).ToDictionary();
+ StateHasChanged();
+ }
+
+ protected override Task OnParametersSetAsync() {
+ OrderBy ??= "name";
+
+ var execute = false;
+
+ foreach (var (key, value) in QueryHelpers.ParseQuery(new Uri(NavigationManager.Uri).Query)) {
+ switch (key) {
+ case "RoomIdContains":
+ Filter.RoomId.Enabled = Filter.RoomId.CheckValueContains = true;
+ Filter.RoomId.ValueContains = value[0]!;
+ break;
+ case "NameContains":
+ Filter.Name.Enabled = Filter.Name.CheckValueContains = true;
+ Filter.Name.ValueContains = value[0]!;
+ break;
+ case "CanonicalAliasContains":
+ Filter.CanonicalAlias.Enabled = Filter.CanonicalAlias.CheckValueContains = true;
+ Filter.CanonicalAlias.ValueContains = value[0]!;
+ break;
+ case "VersionContains":
+ Filter.Version.Enabled = Filter.Version.CheckValueContains = true;
+ Filter.Version.ValueContains = value[0]!;
+ break;
+ case "CreatorContains":
+ Filter.Creator.Enabled = Filter.Creator.CheckValueContains = true;
+ Filter.Creator.ValueContains = value[0]!;
+ break;
+ case "EncryptionContains":
+ Filter.Encryption.Enabled = Filter.Encryption.CheckValueContains = true;
+ Filter.Encryption.ValueContains = value[0]!;
+ break;
+ case "JoinRulesContains":
+ Filter.JoinRules.Enabled = Filter.JoinRules.CheckValueContains = true;
+ Filter.JoinRules.ValueContains = value[0]!;
+ break;
+ case "GuestAccessContains":
+ Filter.GuestAccess.Enabled = Filter.GuestAccess.CheckValueContains = true;
+ Filter.GuestAccess.ValueContains = value[0]!;
+ break;
+ case "HistoryVisibilityContains":
+ Filter.HistoryVisibility.Enabled = Filter.HistoryVisibility.CheckValueContains = true;
+ Filter.HistoryVisibility.ValueContains = value[0]!;
+ break;
+ case "Federatable":
+ Filter.Federation = new() {
+ Enabled = true,
+ Value = bool.Parse(value[0]!)
+ };
+ break;
+ case "Public":
+ Filter.Public = new() {
+ Enabled = true,
+ Value = bool.Parse(value[0]!)
+ };
+ break;
+ case "JoinedMembersGreaterThan":
+ Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true;
+ Filter.JoinedMembers.GreaterThan = int.Parse(value[0]!);
+ break;
+ case "JoinedMembersLessThan":
+ Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true;
+ Filter.JoinedMembers.LessThan = int.Parse(value[0]!);
+ break;
+ case "JoinedLocalMembersGreaterThan":
+ Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true;
+ Filter.JoinedLocalMembers.GreaterThan = int.Parse(value[0]!);
+ break;
+ case "JoinedLocalMembersLessThan":
+ Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true;
+ Filter.JoinedLocalMembers.LessThan = int.Parse(value[0]!);
+ break;
+ case "StateEventsGreaterThan":
+ Filter.StateEvents.Enabled = Filter.StateEvents.CheckGreaterThan = true;
+ Filter.StateEvents.GreaterThan = int.Parse(value[0]!);
+ break;
+ case "StateEventsLessThan":
+ Filter.StateEvents.Enabled = Filter.StateEvents.CheckLessThan = true;
+ Filter.StateEvents.LessThan = int.Parse(value[0]!);
+ break;
+ case "Execute":
+ execute = true;
+ break;
+ case "order_by":
+ case "name_search":
+ case "ascending":
+ case "FetchV12PlusCreatorServer":
+ case "SummarizeLocalMembers":
+ case "FetchTombstones":
+ break;
+ default:
+ Console.WriteLine($"Unknown query parameter: {key}");
+ break;
+ }
+ }
+
+ StateHasChanged();
+
+ if (execute)
+ _ = Search();
+
+ return Task.CompletedTask;
+ }
+
+ private async Task Search() {
+ Results.Clear();
+ Console.WriteLine("Starting search... Parameters: " + new {
+ orderBy = OrderBy!,
+ dir = Ascending ? "f" : "b",
+ searchTerm = SearchTerm,
+ localFilter = Filter,
+ chunkLimit = 1000,
+ fetchTombstones = FetchTombstones,
+ fetchTopics = true,
+ fetchCreateEvents = true
+ }.ToJson());
+ var searchRooms = Homeserver.Admin.SearchRoomsAsync(
+ orderBy: OrderBy!,
+ dir: Ascending ? "f" : "b",
+ searchTerm: SearchTerm,
+ localFilter: Filter,
+ chunkLimit: 1000,
+ fetchTombstones: FetchTombstones,
+ fetchTopics: true,
+ fetchCreateEvents: true
+ ).GetAsyncEnumerator();
+ var joinedRooms = await Homeserver.GetJoinedRooms();
+ while (await searchRooms.MoveNextAsync()) {
+ var room = searchRooms.Current;
+
+ 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,
+ OriginHomeserver =
+ Homeserver.GetRoom(room.RoomId).IsV12PlusRoomId
+ ? room.RoomId.Split(':', 2).Skip(1).FirstOrDefault(string.Empty)
+ : string.Empty
+ };
+
+ if (string.IsNullOrWhiteSpace(roomInfo.OriginHomeserver) && FetchV12PlusCreatorServer) {
+ try {
+ if (joinedRooms.Any(x => x.RoomId == room.RoomId))
+ roomInfo.OriginHomeserver = await Homeserver.GetRoom(room.RoomId).GetOriginHomeserverAsync();
+ else roomInfo.OriginHomeserver = (await Homeserver.Admin.GetRoomStateAsync(room.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1];
+ }
+ catch (MatrixException e) {
+ roomInfo.Exceptions.Add($"While getting origin homeserver: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}");
+ }
+ }
+
+ Results.Add(roomInfo);
+
+ if ((Results.Count <= 200 && Results.Count % 10 == 0 && FetchV12PlusCreatorServer) || Results.Count % 1000 == 0) {
+ StateHasChanged();
+ await Task.Yield();
+ await Task.Delay(1);
+ }
+ }
+
+ StateHasChanged();
+
+ if (FetchV12PlusCreatorServer) await FetchV12PlusCreatorServersAsync(false);
+ if (SummarizeLocalMembers) await FetchLocalMemberEventsAsync(false);
+ // if (CheckTombstone) await FetchTombstoneEventsAsync(false);
+
+ StateHasChanged();
+ }
+
+ private Task DeleteRoom(RoomInfo room, bool executeWithoutConfirmation = false) {
+ var dc = JsonSerializer.Deserialize<SynapseRoomShutdownWindowContent.RoomShutdownContext>(DefaultShutdownContext.ToJson())!;
+ dc.RoomId = room.RoomId;
+ dc.RoomDetails = room;
+ dc.ExecuteImmediately = executeWithoutConfirmation;
+ DeleteRequests.TryAdd(room.RoomId, dc);
+ StateHasChanged();
+
+ return Task.CompletedTask;
+ }
+
+ private void PurgeSelection() {
+ foreach (var room in Results.Where(x => x.MultiPurgeSelected)) {
+ DeleteRoom(room, true);
+ }
+ }
+
+ 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; }
+ public required string OriginHomeserver { get; set; }
+
+ [field: AllowNull, MaybeNull]
+ public string MemberSummary => field ??= $"{JoinedMembers} members, of which {JoinedLocalMembers} are on this server";
+
+ public List<string> Exceptions { get; set; } = [];
+ public bool MultiPurgeSelected { get; set; }
+ }
+
+ private async Task ExportState(RoomInfo room) {
+ try {
+ var state = await Homeserver.Admin.GetRoomStateAsync(room.RoomId);
+ var json = state.ToJson();
+ await BlazorSaveFileService.SaveFileAsync($"{room.RoomId.Replace(":", "_")}_state.json", System.Text.Encoding.UTF8.GetBytes(json), "application/json");
+ }
+ catch (Exception e) {
+ Logger.LogError(e, "Failed to export room state for {RoomId}", room.RoomId);
+ }
+ }
+
+ private async Task ForceJoin(RoomInfo room) {
+ try {
+ await Homeserver.GetRoom(room.RoomId).JoinAsync([Homeserver.ServerName]);
+ }
+ catch (Exception e) {
+ Logger.LogError(e, "Failed to force-join room {RoomId}", room.RoomId);
+ // await Homeserver.Admin.room
+ }
+ }
+
+ private SemaphoreSlim _concurrencyLimiter = new SemaphoreSlim(16, 16);
+
+ private async Task FetchV12PlusCreatorServersAsync() => await FetchV12PlusCreatorServersAsync(true);
+
+ private async Task FetchV12PlusCreatorServersAsync(bool rerender) {
+ var joinedRooms = await Homeserver.GetJoinedRooms();
+ var tasks = Results
+ .Where(x => string.IsNullOrWhiteSpace(x.OriginHomeserver))
+ .Select(async r => {
+ if (!string.IsNullOrWhiteSpace(r.Creator) && r.Creator.Contains(':')) {
+ r.OriginHomeserver = r.Creator.Split(':', 2)[1];
+ return;
+ }
+
+ if (r.CreateEvent != null && !string.IsNullOrWhiteSpace(r.CreateEvent.Sender) && r.CreateEvent.Sender.Contains(':')) {
+ r.OriginHomeserver = r.CreateEvent.Sender.Split(':', 2)[1];
+ return;
+ }
+
+ await _concurrencyLimiter.WaitAsync();
+ try {
+ if (joinedRooms.Any(x => x.RoomId == r.RoomId))
+ r.OriginHomeserver = await Homeserver.GetRoom(r.RoomId).GetOriginHomeserverAsync();
+ else r.OriginHomeserver = (await Homeserver.Admin.GetRoomStateAsync(r.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1];
+ }
+ catch (MatrixException e) {
+ r.Exceptions.Add($"While getting origin homeserver: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}");
+ }
+ catch (Exception e) {
+ Console.WriteLine($"Failed to get origin homeserver for {r.RoomId}, unhandled exception: " + e);
+ }
+ finally {
+ _concurrencyLimiter.Release();
+ }
+ });
+
+ await Task.WhenAll(tasks);
+
+ if (rerender)
+ StateHasChanged();
+ }
+
+ private async Task FetchTombstoneEventsAsync() => await FetchTombstoneEventsAsync(true);
+
+ private async Task FetchTombstoneEventsAsync(bool rerender) {
+ var getTombstoneTasks = Results
+ .Where(x => x.TombstoneEvent is null)
+ .Select(async r => {
+ await _concurrencyLimiter.WaitAsync();
+ try {
+ var state = await Homeserver.Admin.GetRoomStateAsync(r.RoomId, type: "m.room.tombstone");
+ var tombstone = state.Events.FirstOrDefault(x => x is { StateKey: "", Type: "m.room.tombstone" });
+ if (tombstone is { } tombstoneEvent) {
+ r.TombstoneEvent = tombstoneEvent;
+ }
+ }
+ catch (MatrixException e) {
+ r.Exceptions.Add($"While checking for tombstone: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}");
+ }
+ catch (Exception e) {
+ Console.WriteLine($"Failed to check tombstone for {r.RoomId}, unhandled exception: " + e);
+ }
+ finally {
+ _concurrencyLimiter.Release();
+ }
+ });
+
+ await Task.WhenAll(getTombstoneTasks);
+
+ if (rerender)
+ StateHasChanged();
+ }
+
+ private async Task FetchLocalMemberEventsAsync() => await FetchLocalMemberEventsAsync(true);
+
+ private async Task FetchLocalMemberEventsAsync(bool rerender) {
+ var getLocalMembersTasks = Results
+ .Where(x => x.LocalMembers is null && x.JoinedLocalMembers is > 0 and < 100)
+ .Select(async r => {
+ await _concurrencyLimiter.WaitAsync();
+ try {
+ var members = (await Homeserver.Admin.GetRoomMembersAsync(r.RoomId)).Members.Where(x => x.EndsWith(":" + Homeserver.ServerName)).ToList();
+ r.LocalMembers = members;
+ }
+ catch (MatrixException e) {
+ r.Exceptions.Add($"While fetching local members: {e.GetAsObject().ToJson(ignoreNull: true, indent: false)}");
+ }
+ catch (Exception e) {
+ Console.WriteLine($"Failed to fetch local members for {r.RoomId}, unhandled exception: " + e);
+ }
+ finally {
+ _concurrencyLimiter.Release();
+ }
+ });
+
+ await Task.WhenAll(getLocalMembersTasks);
+
+ if (rerender)
+ StateHasChanged();
+ }
+
+ private void MultiPurgeInvertSelection() {
+ foreach (var room in Results) {
+ room.MultiPurgeSelected ^= true;
+ }
+
+ StateHasChanged();
+ }
+
+}
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..62941e5
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css
@@ -0,0 +1,7 @@
+.room-list-item {
+ background-color: #ffffff11;
+ border-radius: 0.5em;
+ display: block;
+ margin-top: 4px;
+ padding: 4px;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor
new file mode 100644
index 0000000..c2446a2
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor
@@ -0,0 +1,212 @@
+@page "/HSAdmin/Synapse/ResyncState"
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests
+
+<h3>Resync room state with other server</h3>
+<hr/>
+
+@if (!Executing) {
+ <p>WARNING: Will likely not work on invite-only/knock rooms! May also mess with history visibility!</p>
+ <p>If the room is using mjolnir/draupnir, it's probably recommended to set the "via" to the server it's hosted on.</p>
+ <span>Room ID: </span>
+ <InputText @bind-Value="@RoomId"></InputText>
+ <br/>
+ <span>Via: </span>
+ <InputText @bind-Value="@Via"></InputText>
+ <br/>
+ <LinkButton OnClickAsync="@Execute">Execute</LinkButton>
+}
+
+@if (Executing) {
+ <p>Execution in progress. DO NOT CLOSE THIS PAGE!</p>
+}
+@* stage 1 *@
+@if (Stage >= 1) {
+ @if (Members is null) {
+ <p>Loading members...</p>
+ }
+ else {
+ <p>Got @Members.Count local members</p>
+ }
+}
+
+@* stage 2 *@
+@if (Stage == 2) {
+ <p>Purging room, please wait...</p>
+ <pre>@DeleteStatus.ToJson(ignoreNull: true)</pre>
+}
+
+@* stage 3 *@
+
+@if (Stage == 3) {
+ <p>Rejoining room, please wait...</p>
+ <p>Members left to restore: </p>
+ string members = "";
+ foreach (var member in Members) {
+ members += $"{member.StateKey} ({member.ContentAs<RoomMemberEventContent>()?.ToJson(indent: false, ignoreNull: true)})\n";
+ }
+
+ <pre>
+ @members
+ </pre>
+}
+
+@if (Stage == 4) {
+ <p>Execution finished. You may now close the page :)</p>
+}
+
+@if (Error is not null) {
+ <p style="color: red">Error: @Error.Message</p>
+ <pre>
+ @Error.ToString()
+ </pre>
+}
+
+@code {
+
+ [Parameter]
+ [SupplyParameterFromQuery]
+ public string? RoomId { get; set; }
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "via")]
+ public string? Via { get; set; }
+
+ private AuthenticatedHomeserverSynapse? Homeserver { get; set; }
+
+ // Execution flow
+ private int Stage { get; set; }
+ private bool Executing { get; set; }
+ private Exception? Error { get; set; }
+
+ // Stage 1
+ private List<StateEventResponse>? Members { get; set; }
+
+ // Stage 2
+ private SynapseAdminRoomDeleteStatus? DeleteStatus { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not AuthenticatedHomeserverSynapse hs) return;
+ Homeserver = hs;
+
+ StateHasChanged();
+ }
+
+ private Task Execute() => Execute(0);
+
+ private async Task Execute(int startStage) {
+ if (string.IsNullOrWhiteSpace(RoomId)) return;
+ if (string.IsNullOrWhiteSpace(Via)) return;
+ Executing = true;
+ StateHasChanged();
+
+ await ExecuteStages(startStage);
+
+ StateHasChanged();
+ }
+
+ private async Task ExecuteStages(int startStage) {
+ if (startStage <= 1)
+ if (!await TryGetRoomMembers())
+ return;
+ if (startStage <= 2)
+ if (!await TryPurgeRoom())
+ return;
+ if (startStage <= 3)
+ if (!await TryRestoreRoom())
+ return;
+
+ Stage = 4;
+ Executing = false;
+ StateHasChanged();
+ }
+
+ private async Task<bool> TryGetRoomMembers() {
+ Stage = 1;
+ try {
+ Members = (await Homeserver.Admin.GetRoomStateAsync(RoomId, type: RoomMemberEventContent.EventId))
+ .Events.Where(m => (m.StateKey?.EndsWith(':' + Homeserver.ServerName) ?? false) && m.ContentAs<RoomMemberEventContent>()!.Membership == "join")
+ .ToList();
+ Console.WriteLine(Members.ToJson(ignoreNull: true));
+ StateHasChanged();
+ return true;
+ }
+ catch (Exception e) {
+ Error = e;
+ return Executing = false;
+ }
+ }
+
+ private async Task<bool> TryPurgeRoom() {
+ Stage = 2;
+
+ try {
+ var resp = await Homeserver.Admin.DeleteRoom(RoomId, new SynapseAdminRoomDeleteRequest {
+ Block = true,
+ Purge = true,
+ // ForcePurge = true // This causes synapse to early return and not actually purge stuff...
+ }, waitForCompletion: false);
+
+ while (true) {
+ // we dont want API failure to break this step
+ try {
+ DeleteStatus = await Homeserver.Admin.GetRoomDeleteStatus(resp.DeleteId);
+ StateHasChanged();
+ if (DeleteStatus.Status == SynapseAdminRoomDeleteStatus.Complete) {
+ return true;
+ }
+
+ if (DeleteStatus.Status == SynapseAdminRoomDeleteStatus.Failed) {
+ Error = new Exception("Failed to delete room: " + DeleteStatus.ToJson());
+ return Executing = false;
+ }
+
+ await Task.Delay(1000);
+ }
+ catch { }
+ }
+
+ StateHasChanged();
+ return true;
+ }
+ catch (Exception e) {
+ Error = e;
+ return Executing = false;
+ }
+ }
+
+ private async Task<bool> TryRestoreRoom() {
+ Stage = 3;
+ try {
+ await Homeserver.Admin.BlockRoom(RoomId, block: false);
+ Members = Random.Shared.GetItems(Members.ToArray(), Members.Count).ToList();
+ StateHasChanged();
+ foreach (var member in Members) {
+ while (true) {
+ try {
+ var hs = member.StateKey == Homeserver.WhoAmI.UserId
+ ? Homeserver
+ : await Homeserver.Admin.GetHomeserverForUserAsync(member.StateKey!, TimeSpan.FromMinutes(120));
+ await hs.GetRoom(RoomId).JoinAsync([Via], reason: "Reconciling state with " + Via, false);
+ await hs.GetRoom(RoomId).SendStateEventAsync(RoomMemberEventContent.EventId, member.StateKey, member.RawContent);
+ Members = Members.Skip(1).ToList();
+ StateHasChanged();
+ break;
+ }
+ catch (Exception e) {
+ Error = new Exception($"{DateTime.Now:u} Failed to join room: {member.StateKey}, retrying\n", e);
+ }
+ }
+ }
+
+ return true;
+ }
+ catch (Exception e) {
+ Error = e;
+ return Executing = false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor
new file mode 100644
index 0000000..54ac800
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor
@@ -0,0 +1,243 @@
+@page "/HSAdmin/Synapse/UserQuery"
+@using Microsoft.AspNetCore.WebUtilities
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Homeservers.Extensions.NamedCaches
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses
+@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components
+@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components.RoomQuery
+@inject ILogger<RoomQuery> Logger
+
+<h3>Homeserver Administration - User 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>
+ @* <SynapseRoomQueryFilter Filter="@Filter"/> *@
+</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 user in Results) {
+ <div class="room-list-item">
+ <p>
+ <span>@user.Name</span>
+ @if (!string.IsNullOrWhiteSpace(user.DisplayName)) {
+ <span> (@user.DisplayName)</span>
+ }
+ <br/>
+ </p>
+ <p>
+ <LinkButton OnClickAsync="@(() => Login(user))">Log in</LinkButton>
+ @* <LinkButton OnClickAsync="@(() => DeleteRoom(user))">Delete room</LinkButton> *@
+ @* <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={user.RoomId}&via={user.RoomId.Split(':', 2)[1]}")">Resync state</LinkButton> *@
+
+ </p>
+
+ @{
+ List<string?> flags = [];
+ if (user.IsGuest == true) flags.Add("guest");
+ if (user.Admin == true) flags.Add("admin");
+ if (user.Deactivated == true) flags.Add("deactivated");
+ if (user.Erased == true) flags.Add("erased");
+ if (user.ShadowBanned == true) flags.Add("shadow banned");
+ if (user.Locked == true) flags.Add("locked");
+ if (user.Approved == true) flags.Add("approved");
+
+ if (!string.IsNullOrWhiteSpace(user.UserType)) flags.Add($"type=\"{user.UserType}\"");
+
+ flags = flags.Where(x => x != null).ToList();
+ }
+ <span>@string.Join(", ", flags)</span>
+ <br/>
+
+ <details>
+ <summary>Full result data</summary>
+ <pre>@user.ToJson(ignoreNull: true)</pre>
+ </details>
+ </div>
+}
+
+@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; } = true;
+
+ private List<SynapseAdminUserListResult.SynapseAdminUserListResultUser> Results { get; set; } = new();
+
+ private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!;
+
+ private SynapseAdminLocalUserQueryFilter Filter { get; set; } = new();
+
+ protected override async Task OnInitializedAsync() {
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is not AuthenticatedHomeserverSynapse synapse) {
+ NavigationManager.NavigateTo("/");
+ return;
+ }
+
+ Homeserver = synapse;
+ StateHasChanged();
+ }
+
+ protected override Task OnParametersSetAsync() {
+ 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 searchRooms = Homeserver.Admin.SearchUsersAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", localFilter: Filter).GetAsyncEnumerator();
+ while (await searchRooms.MoveNextAsync()) {
+ var room = searchRooms.Current;
+
+ Results.Add(room);
+
+ if ((Results.Count <= 200 && Results.Count % 10 == 0) || Results.Count % 1000 == 0) {
+ StateHasChanged();
+ await Task.Yield();
+ await Task.Delay(1);
+ }
+ }
+
+ StateHasChanged();
+
+ StateHasChanged();
+ }
+
+ private readonly Dictionary<string, string> validOrderBy = new() {
+ { "name", "User name" },
+ { "is_guest", "Guest status" },
+ { "admin", "Admin status" },
+ { "user_type", "User type" },
+ { "deactivated", "Deactivation status" },
+ { "shadow_banned", "Shadow banned status" },
+ { "displayname", "Display name" },
+ { "avatar_url", "Avatar URL" },
+ { "creation_ts", "Creation time" },
+ { "last_seen_ts", "Last activity" },
+ };
+
+ private async Task Login(SynapseAdminUserListResult.SynapseAdminUserListResultUser user) {
+ var loginResult = await Homeserver.Admin.LoginUserAsync(user.Name, TimeSpan.FromDays(1));
+ await sessionStore.AddSession(new() {
+ AccessToken = loginResult.AccessToken,
+ DeviceId = loginResult.DeviceId,
+ UserId = loginResult.UserId,
+ Homeserver = Homeserver.ServerName,
+ Proxy = Homeserver.Proxy
+ });
+
+ }
+
+}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css
new file mode 100644
index 0000000..62941e5
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css
@@ -0,0 +1,7 @@
+.room-list-item {
+ background-color: #ffffff11;
+ border-radius: 0.5em;
+ display: block;
+ margin-top: 4px;
+ padding: 4px;
+}
\ No newline at end of file
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..82ee0f2 100644
--- a/MatrixUtils.Web/Pages/Index.razor
+++ b/MatrixUtils.Web/Pages/Index.razor
@@ -4,6 +4,7 @@
@using LibMatrix
@using ArcaneLibs
@using System.Diagnostics
+@using LibMatrix.Responses.Federation
<PageTitle>Index</PageTitle>
@@ -19,23 +20,32 @@ Small collection of tools to do not-so-everyday things.
</span>
}
<hr/>
-<form>
+<form aria-busy="@Busy">
<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 +54,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 OnClickAsync="@(() => ManageUser(session.SessionId))">Manage</LinkButton>
+ <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
+ <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId, true))">Log out</LinkButton>
</p>
</td>
</tr>
@@ -70,16 +81,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 OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
</td>
</tr>
}
@@ -99,19 +110,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 OnClickAsync="@(() => Task.Run(() => NavigationManager.NavigateTo($"/InvalidSession?ctx={session.SessionId}")))">Re-login</LinkButton>
</td>
<td>
- <LinkButton OnClick="@(() => RemoveUser(session))">Remove</LinkButton>
+ <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
</td>
</tr>
}
@@ -127,67 +138,78 @@ 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 bool Busy { get; set; } = true;
+
+ 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 +219,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);
@@ -222,19 +244,22 @@ Small collection of tools to do not-so-everyday things.
await Task.WhenAll(tasks);
scannedSessions = totalSessions;
- await base.OnInitializedAsync();
+ Busy = false;
+ StateHasChanged();
+ Console.WriteLine("Index.OnInitializedAsync finished");
}
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 +271,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..f86d112 100644
--- a/MatrixUtils.Web/Pages/InvalidSession.razor
+++ b/MatrixUtils.Web/Pages/InvalidSession.razor
@@ -6,15 +6,16 @@
<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>
- <LinkButton OnClick="@(OpenRefreshDialog)">Refresh token</LinkButton>
- <LinkButton OnClick="@(RemoveUser)">Remove</LinkButton>
+@if (_auth is not null) {
+ <p>It appears that the affected user is @_auth.UserId (@_auth.DeviceId) on @_auth.Homeserver!</p>
+ <LinkButton OnClickAsync="@(OpenRefreshDialog)">Refresh token</LinkButton>
+ <LinkButton OnClickAsync="@(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/>
- <LinkButton OnClick="TryLogin">Log in</LinkButton>
+ <ModalWindow MinWidth="300" X="275" Y="300" Title="@($"Password for {_auth.UserId}")">
+ <FancyTextBox IsPassword="true" @bind-Value="@_password"></FancyTextBox>
+ <br/>
+ <LinkButton OnClickAsync="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..56c8cfe 100644
--- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
+++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
@@ -1,7 +1,7 @@
@using ClientContext = MatrixUtils.Web.Pages.Labs.Client.Index.ClientContext
@* user header and room list *@
@foreach (var room in Data.SyncWrapper.Rooms) {
- <LinkButton OnClick="@(async () => Data.SelectedRoom = room)" Color="@(Data.SelectedRoom == room ? "#FF00FF" : "")">
+ <LinkButton OnClickAsync="@(async () => Data.SelectedRoom = room)" Color="@(Data.SelectedRoom == room ? "#FF00FF" : "")">
@room.RoomName
</LinkButton>
<br/>
@@ -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..7199934 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>
@@ -25,10 +26,10 @@
<InputCheckbox @bind-Value="SetupData.DmSpaceInfo.LayerByUser"></InputCheckbox>
Create sub-spaces per user
</p>
-
+
<br/>
- <LinkButton OnClick="@Disband" Color="#FF0000">Disband</LinkButton>
- <LinkButton OnClick="@Execute">Next</LinkButton>
+ <LinkButton OnClickAsync="@Disband" Color="#FF0000">Disband</LinkButton>
+ <LinkButton OnClickAsync="@Execute">Next</LinkButton>
}
else {
<p>Discovering spaces, please wait...</p>
@@ -77,7 +78,7 @@ else {
userRooms.Add(room);
}
- var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncEnumerable();
+ var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncResultEnumerable();
await foreach (var room in roomChecks)
if (room.HasValue)
spaces.TryAdd(room.Value.id, room.Value.roomInfo);
@@ -108,8 +109,8 @@ else {
public async Task<(string id, RoomInfo roomInfo)?> GetFeasibleSpaces(GenericRoom room) {
try {
var ri = new RoomInfo(room);
-
- await foreach(var evt in room.GetFullStateAsync())
+
+ await foreach (var evt in room.GetFullStateAsync())
ri.StateEvents.Add(evt);
var powerLevels = (await ri.GetStateEvent(RoomPowerLevelEventContent.EventId)).TypedContent as RoomPowerLevelEventContent;
@@ -117,7 +118,7 @@ else {
Console.WriteLine($"No permission to send m.space.child in {room.RoomId}...");
return null;
}
-
+
Status = $"Found viable space: {ri.RoomName}";
if (!string.IsNullOrWhiteSpace(SetupData.DmSpaceConfiguration!.DMSpaceId)) {
if (await room.GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) is { } dsi) {
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
index be6027a..ed65e94 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>
@@ -31,17 +31,19 @@ else {
}
<br/>
-<LinkButton OnClick="@Execute">Next</LinkButton>
+<LinkButton OnClickAsync="@Execute">Next</LinkButton>
@{
var _offset = 0;
}
@foreach (var (room, usersList) in duplicateDmRooms) {
<ModalWindow Title="Duplicate room found" X="_offset += 30" Y="_offset">
- <p>Found room assigned to multiple users: <RoomListItem RoomInfo="@room"></RoomListItem></p>
+ <p>Found room assigned to multiple users:
+ <RoomListItem RoomInfo="@room"></RoomListItem>
+ </p>
<p>Users:</p>
@foreach (var userProfileResponse in usersList) {
- <LinkButton OnClick="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))">
+ <LinkButton OnClickAsync="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))">
<span>Assign to </span>
<InlineUserItem User="userProfileResponse"></InlineUserItem>
</LinkButton>
@@ -54,7 +56,7 @@ else {
<ModalWindow Title="Re-assign DM" OnCloseClicked="@(() => DmToReassign = null)">
<RoomListItem RoomInfo="@DmToReassign"></RoomListItem>
@foreach (var userProfileResponse in roomMembers[DmToReassign]) {
- <LinkButton OnClick="@(() => SetRoomAssignment(DmToReassign.Room.RoomId, userProfileResponse.Id))">
+ <LinkButton OnClickAsync="@(() => SetRoomAssignment(DmToReassign.Room.RoomId, userProfileResponse.Id))">
<span>Assign to </span>
<InlineUserItem User="userProfileResponse"></InlineUserItem>
</LinkButton>
@@ -141,12 +143,12 @@ else {
}
var roomList = new List<RoomInfo>();
- var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable();
+ var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncResultEnumerable();
await foreach (var result in tasks)
roomList.Add(result);
return (userProfile, roomList);
// StateHasChanged();
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var res in results) {
SetupData.DMRooms.Add(res.userProfile, res.roomList);
// Status = $"Listed {dmRooms.Count} users";
@@ -181,18 +183,18 @@ else {
await roomInfo.FetchAllStateAsync();
roomMembers[roomInfo] = new();
// roomInfo.CreationEventContent = await room.GetCreateEventAsync();
-
- if(roomInfo.RoomName == room.RoomId)
+
+ if (roomInfo.RoomName == room.RoomId)
try {
roomInfo.RoomName = await room.GetNameOrFallbackAsync();
}
catch { }
- var membersEnum = room.GetMembersEnumerableAsync(true);
+ var membersEnum = room.GetMembersEnumerableAsync("join");
await foreach (var member in membersEnum)
if (member.TypedContent is RoomMemberEventContent memberEvent)
roomMembers[roomInfo].Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey });
-
+
try {
string? roomIcon = (await room.GetAvatarUrlAsync())?.Url;
if (room is not null)
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
index 09de5d3..686894c 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>
@@ -59,7 +59,7 @@ else {
}
<br/>
-<LinkButton OnClick="@Execute">Next</LinkButton>
+<LinkButton OnClickAsync="@Execute">Next</LinkButton>
@code {
@@ -115,11 +115,11 @@ else {
// };
// }
// var roomList = new List<RoomInfo>();
- // var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable();
+ // var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncResultEnumerable();
// await foreach (var result in tasks)
// roomList.Add(result);
// return (userProfile, roomList);
- // }).ToAsyncEnumerable();
+ // }).ToAsyncResultEnumerable();
// await foreach (var res in results) {
// dmRooms.Add(new RoomInfo() {
// Room = dmSpaceRoom,
@@ -150,7 +150,7 @@ else {
}
catch { }
- var membersEnum = room.GetMembersEnumerableAsync(true);
+ var membersEnum = room.GetMembersEnumerableAsync("join");
await foreach (var member in membersEnum)
if (member.TypedContent is RoomMemberEventContent memberEvent)
roomMembers.Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey });
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..dd217e9 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) => {
@@ -36,7 +36,6 @@
}
//debounce StateHasChanged, we dont want to reredner on every key stroke
-
private CancellationTokenSource _debounceCts = new CancellationTokenSource();
private async Task DebouncedStateHasChanged() {
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..38ede74 100644
--- a/MatrixUtils.Web/Pages/LoginPage.razor
+++ b/MatrixUtils.Web/Pages/LoginPage.razor
@@ -22,8 +22,29 @@
<FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox>
</span>
<br/>
-<LinkButton OnClick="@AddRecord">Add account to queue</LinkButton>
-<LinkButton OnClick="@(() => Login(newRecordInput))">Log in</LinkButton>
+<LinkButton OnClickAsync="@AddRecord">Add account to queue</LinkButton>
+<LinkButton OnClickAsync="@(() => Login(newRecordInput))">Log in</LinkButton>
+<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 OnClickAsync="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton>
<br/>
<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>
@@ -80,14 +101,14 @@
}
</table>
<br/>
-<LinkButton OnClick="@LoginAll">Log in</LinkButton>
+<LinkButton OnClickAsync="@LoginAll">Log in</LinkButton>
@code {
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..17dd554 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>
@@ -19,6 +19,7 @@ else {
else if (checkedRooms.Count > 1) {
<p>Done!</p>
}
+
@foreach (var (state, rooms) in matchingStates) {
<u>@state</u>
<br/>
@@ -44,11 +45,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();
@@ -71,13 +72,14 @@ else {
_semaphoreSlim.Release();
return; //abort if changed
}
+
matchingStates.Clear();
foreach (var homeserver in hss) {
currentHs = homeserver;
var rooms = await homeserver.GetJoinedRooms();
rooms.RemoveAll(x => checkedRooms.Contains(x.RoomId));
checkedRooms.AddRange(rooms.Select(x => x.RoomId));
- var tasks = rooms.Select(x => GetMembershipAsync(x, mxid)).ToAsyncEnumerable();
+ var tasks = rooms.Select(x => GetMembershipAsync(x, mxid)).ToAsyncResultEnumerable();
await foreach (var (room, state) in tasks) {
if (state is null) continue;
if (!matchingStates.ContainsKey(state.Membership))
@@ -97,8 +99,10 @@ else {
return; //abort if changed
}
}
+
StateHasChanged();
}
+
currentHs = null;
StateHasChanged();
_semaphoreSlim.Release();
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/Create2.razor b/MatrixUtils.Web/Pages/Rooms/Create2.razor
new file mode 100644
index 0000000..4a29847
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/Create2.razor
@@ -0,0 +1,147 @@
+@page "/Rooms/Create2"
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.Helpers
+@using LibMatrix.Responses
+@using LibMatrix.RoomTypes
+@using MatrixUtils.Web.Pages.Rooms.RoomCreateComponents
+@inject ILogger<Create2> logger
+@* @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not */ *@
+
+<h3>Room Manager - Create Room</h3>
+
+@if (Ready) {
+ <style>
+ table.table-top-first-tr tr td:first-child {
+ vertical-align: top;
+ }
+ </style>
+ <table class="table-top-first-tr">
+ @if (roomBuilder is RoomUpgradeBuilder roomUpgrade) {
+ <RoomCreateUpgradeOptions roomUpgrade="@roomUpgrade" PageStateHasChanged="@StateHasChanged" OldRoom="@PreviousRoom" />
+ }
+ else {
+ @* <tr style="padding-bottom: 16px;"> *@
+ @* <td>Preset:</td> *@
+ @* <td> *@
+ @* @if (Presets is null) { *@
+ @* <p style="color: red;">Presets is null!</p> *@
+ @* } *@
+ @* else { *@
+ @* <p style="color: red;">Support for presets is currently disabled!</p> *@
+ @* $1$ <InputSelect @bind-Value="@RoomPreset"> #1# *@
+ @* $1$ @foreach (var createRoomRequest in Presets) { #1# *@
+ @* $1$ <option value="@createRoomRequest.Key">@createRoomRequest.Key</option> #1# *@
+ @* $1$ } #1# *@
+ @* $1$ </InputSelect> #1# *@
+ @* } *@
+ @* </td> *@
+ @* </tr> *@
+ }
+ <RoomCreateBasicRoomInfoOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/>
+ <RoomCreateCreateOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/>
+ <RoomCreatePrivacyOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/>
+ <RoomCreatePermissionsOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/>
+ <RoomCreateMembershipOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/>
+ @* Initial states, should remain at bottom *@
+ <RoomCreateInitialStateOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/>
+ </table>
+ <LinkButton OnClickAsync="@CreateRoom">Create room</LinkButton>
+}
+
+<RoomCreateStateDisplay @bind-RoomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged"/>
+
+@if (_matrixException is not null) {
+ <ModalWindow Title="@("Matrix exception: " + _matrixException.ErrorCode)">
+ <pre>
+ @_matrixException.Message
+ </pre>
+ </ModalWindow>
+}
+
+@code {
+
+#region State
+
+ [Parameter, SupplyParameterFromQuery(Name = "previousRoomId")]
+ public string? PreviousRoomId { get; set; }
+
+ public GenericRoom? PreviousRoom { get; set; }
+
+ private bool Ready { get; set; }
+
+ private RoomBuilder roomBuilder { get; set; } = new();
+
+ private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+
+ private MatrixException? _matrixException { get; set; }
+
+#endregion
+
+#region Presets
+
+ private Dictionary<string, CreateRoomRequest>? Presets { get; set; } = new();
+ // private string RoomPreset {
+ // get => Presets.ContainsValue(roomBuilder) ? Presets.First(x => x.Value == roomBuilder).Key : "Not a preset";
+ // set {
+ // roomBuilder = Presets[value];
+ // JsonChanged();
+ // StateHasChanged();
+ // }
+ // }
+
+#endregion
+
+ protected override async Task OnInitializedAsync() {
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (Homeserver is null) return;
+ if (!string.IsNullOrWhiteSpace(PreviousRoomId)) {
+ roomBuilder = new RoomUpgradeBuilder();
+ PreviousRoom = Homeserver.GetRoom(PreviousRoomId);
+ }
+
+ roomBuilder.ServerAcls.Allow = ["*"];
+ roomBuilder.ServerAcls.Deny = [];
+
+ // foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) {
+ // Console.WriteLine($"Found room creation template in class: {x.FullName}");
+ // var instance = (IRoomCreationTemplate)Activator.CreateInstance(x);
+ // Presets[instance.Name] = instance.CreateRoomRequest;
+ // }
+ //
+ // Presets = Presets.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
+
+ // if (!Presets.ContainsKey("Default")) {
+ // Console.WriteLine($"No default room found in {Presets.Count} presets: {string.Join(", ", Presets.Keys)}");
+ // }
+ // else RoomPreset = "Default";
+
+ Ready = true;
+ StateHasChanged();
+ if (roomBuilder is RoomUpgradeBuilder roomUpgrade) {
+ // await roomUpgrade.ImportAsync().ConfigureAwait(false);
+ StateHasChanged();
+ }
+ }
+
+ protected override bool ShouldRender() {
+ if (roomBuilder.Type == "")
+ roomBuilder.Type = null; // Reset to null if empty, so it doesn't get sent as an empty string
+ var result = base.ShouldRender();
+ logger.LogInformation("ShouldRender: " + result);
+ return result;
+ }
+
+ private async Task CreateRoom() {
+ Console.WriteLine("Create room");
+ Console.WriteLine(roomBuilder.ToJson());
+ roomBuilder.AdditionalCreationContent["gay.rory.created_using"] = "Rory&::MatrixUtils (https://mru.rory.gay)";
+ try {
+ var newRoom = await roomBuilder.Create(Homeserver);
+ }
+ catch (MatrixException e) {
+ _matrixException = e;
+ }
+ }
+
+}
diff --git a/MatrixUtils.Web/Pages/Rooms/Index.razor b/MatrixUtils.Web/Pages/Rooms/Index.razor
index 28c4de2..115c903 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,14 +66,14 @@
// 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);
GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId);
var filter = await Homeserver.NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetBasicRoomInfo);
- var filterData = await Homeserver.GetFilterAsync(filter);
+ // var filterData = await Homeserver.GetFilterAsync(filter);
// Rooms = new ObservableCollection<RoomInfo>(rooms.Select(room => new RoomInfo(room)));
// foreach (var stateType in filterData.Room?.State?.Types ?? []) {
@@ -99,7 +97,8 @@
syncHelper = new SyncHelper(Homeserver, logger) {
Timeout = 30000,
FilterId = filter,
- MinimumDelay = TimeSpan.FromMilliseconds(5000)
+ MinimumDelay = TimeSpan.FromMilliseconds(5000),
+ UseMsc4222StateAfter = true
};
// profileSyncHelper = new SyncHelper(Homeserver, logger) {
// Timeout = 10000,
@@ -108,9 +107,9 @@
// };
// profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId);
- RunSyncLoop(syncHelper);
+ _ = RunSyncLoop(syncHelper);
// RunSyncLoop(profileSyncHelper);
- RunQueueProcessor();
+ _ = RunQueueProcessor();
await base.OnInitializedAsync();
}
@@ -122,7 +121,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,16 +130,16 @@
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}!");
- room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.State?.Events);
+ // Console.WriteLine($"QueueWorker: encountered new room {roomId}!");
+ room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.StateAfter?.Events);
Rooms.Add(room);
}
@@ -149,12 +148,16 @@
throw new InvalidDataException("Somehow this is null???");
}
- if (roomData.State?.Events is { Count: > 0 })
- room.StateEvents.MergeStateEventLists(roomData.State.Events);
- else {
+ if (roomData is { StateAfter.Events.Count: > 0 })
+ room.StateEvents!.MergeStateEventLists(roomData.StateAfter.Events);
+ else
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;
@@ -178,7 +181,8 @@
get => _status;
set {
_status = value;
- StateHasChanged();
+ // StateHasChanged();
+ Console.WriteLine(value);
}
}
@@ -188,7 +192,8 @@
get => _status2;
set {
_status2 = value;
- StateHasChanged();
+ // StateHasChanged();
+ Console.WriteLine(value);
}
}
@@ -200,34 +205,34 @@
var syncs = syncHelper.EnumerateSyncAsync();
await foreach (var sync in syncs) {
- Console.WriteLine("trying sync");
- if (sync is null) continue;
-
var filter = await Homeserver.GetFilterAsync(syncHelper.FilterId);
Status = $"Got sync with {sync.Rooms?.Join?.Count ?? 0} room updates, next batch: {sync.NextBatch}!";
- if (sync?.Rooms?.Join != null)
+ if (sync.Rooms?.Join != null)
foreach (var joinedRoom in sync.Rooms.Join)
- if ( /*joinedRoom.Value.AccountData?.Events?.Count > 0 ||*/ joinedRoom.Value.State?.Events?.Count > 0) {
- joinedRoom.Value.State.Events.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI?.UserId);
+ if (joinedRoom.Value.StateAfter?.Events?.Count > 0) {
+ joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI.UserId);
// We can't trust servers to give us what we ask for, and this ruins performance
// Thanks, Conduit.
- joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false);
- if (filter.Room?.State?.NotSenders?.Any() ?? false)
- joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender) ?? false);
+ if (filter is { Room.State.Types.Count: > 0 })
+ joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false);
+ if (filter is { Room.State.NotSenders.Count: > 0 })
+ joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender!) ?? false);
queue.Enqueue(joinedRoom);
}
- if (sync.Rooms.Leave is { Count: > 0 })
+ if (sync.Rooms?.Leave is { Count: > 0 })
foreach (var leftRoom in sync.Rooms.Leave)
if (Rooms.Any(x => x.Room.RoomId == leftRoom.Key))
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}";
+ StateHasChanged();
+ await Task.Yield();
}
}
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index b7ebae2..92c6ca5 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -1,124 +1,152 @@
@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
@using System.Collections.Frozen
+@using System.Collections.Immutable
@using System.Reflection
+@using System.Text.Json
@using ArcaneLibs.Attributes
+@using ArcaneLibs.Blazor.Components.Services
@using LibMatrix.EventTypes
+@using LibMatrix.EventTypes.Interop.Draupnir
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using SpawnDev.BlazorJS.WebWorkers
+@using MatrixUtils.Web.Pages.Rooms.PolicyListComponents
+@using SpawnDev.BlazorJS
+@inject WebWorkerService WebWorkerService
+@inject ILogger<PolicyList> logger
+@inject BlazorJSRuntime JsRuntime
-@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>
+@if (!IsInitialised) {
+ <p>Connecting to homeserver...</p>
}
else {
- @foreach (var (type, value) in PolicyEventsByType) {
- <p>
- @(GetValidPolicyEventsByType(type).Count) active,
- @(GetInvalidPolicyEventsByType(type).Count) invalid
- (@value.Count total)
- @(GetPolicyTypeName(type).ToLower())
- </p>
+ <PolicyListEditorHeader Room="@Room" @bind-RenderEventInfo="@RenderEventInfo" ReloadStateAsync="@(() => LoadStateAsync(true))"></PolicyListEditorHeader>
+ @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 value in PolicyCollections.Values.OrderByDescending(x => x.TotalCount)) {
+ <p>
+ @value.ActivePolicies.Count active,
+ @value.RemovedPolicies.Count removed
+ (@value.TotalCount total)
+ @value.Name.ToLower()
+ </p>
+ }
- @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>
- <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
- @{
- 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();
- }
- <thead>
- <tr>
- @foreach (var name in propNames) {
- <th style="border-width: 1px">@name</th>
- }
- <th style="border-width: 1px">Actions</th>
- </tr>
- </thead>
- <tbody style="border-width: 1px;">
- @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) {
- <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>
- }
- <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>
- }
- }
- </div>
- </td>
- </tr>
- }
- </tbody>
- </table>
- <details>
- <summary>
- <u>
- @("Invalid " + GetPolicyTypeName(type).ToLower())
- </u>
- </summary>
- <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
- <thead>
- <tr>
- <th style="border-width: 1px">State key</th>
- <th style="border-width: 1px">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>
- }
-}
+ @if (DuplicateBans?.ActivePolicies.Count > 0) {
+ <p style="color: orange;">
+ Found @DuplicateBans.Value.ActivePolicies.Count duplicate bans
+ </p>
+ }
+
+ @if (RedundantBans?.ActivePolicies.Count > 0) {
+ <p style="color: orange;">
+ Found @RedundantBans.Value.ActivePolicies.Count redundant bans
+ </p>
+ }
+
+ // logger.LogInformation($"Rendered header in {renderSw.GetElapsedAndRestart()}");
+
+ // var renderSw2 = Stopwatch.StartNew();
+ // IOrderedEnumerable<Type> policiesByType = KnownPolicyTypes.Where(t => GetPolicyEventsByType(t).Count > 0).OrderByDescending(t => GetPolicyEventsByType(t).Count);
+ // logger.LogInformation($"Ordered policy types by count in {renderSw2.GetElapsedAndRestart()}");
+
+ @if (DuplicateBans?.ActivePolicies.Count > 0) {
+ <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@DuplicateBans.Value"
+ Room="@Room"></PolicyListCategoryComponent>
+ }
-@if (CurrentlyEditingEvent is not null) {
- <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal>
+ @if (RedundantBans?.ActivePolicies.Count > 0) {
+ <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@RedundantBans.Value"
+ Room="@Room"></PolicyListCategoryComponent>
+ }
+
+ foreach (var collection in PolicyCollections.Values.OrderByDescending(x => x.ActivePolicies.Count)) {
+ <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent>
+ }
+
+ // foreach (var type in policiesByType) {
+ @* foreach (var type in (List<Type>) []) { *@
+ @* <details> *@
+ @* <summary> *@
+ @* <span> *@
+ @* @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") *@
+ @* </span> *@
+ @* <hr style="margin: revert;"/> *@
+ @* </summary> *@
+ @* <table class="table table-striped table-hover table-bordered align-middle"> *@
+ @* @{ *@
+ @* var renderSw3 = Stopwatch.StartNew(); *@
+ @* 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(); *@
+ @* logger.LogInformation($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); *@
+ @* logger.LogInformation($"Filtered policies and got properties in {renderSw3.GetElapsedAndRestart()}"); *@
+ @* } *@
+ @* <thead> *@
+ @* <tr> *@
+ @* @foreach (var name in propNames) { *@
+ @* <th>@name</th> *@
+ @* } *@
+ @* <th>Actions</th> *@
+ @* </tr> *@
+ @* </thead> *@
+ @* <tbody> *@
+ @* @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) { *@
+ @* <PolicyListRowComponent PolicyInfo="@policy" Room="@Room"></PolicyListRowComponent> *@
+ @* } *@
+ @* </tbody> *@
+ @* </table> *@
+ @* <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> *@
+ // }
+
+ // logger.LogInformation($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
+ logger.LogInformation("Rendered in {TimeSpan}", renderTotalSw.Elapsed);
+ }
}
@code {
@@ -129,112 +157,472 @@ else {
private const bool Debug = false;
#endif
+ private bool IsInitialised { get; set; } = false;
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 required string RoomId { get; set; }
- private bool _enableAvatars;
- private StateEventResponse? _currentlyEditingEvent;
-
- // static readonly Dictionary<string, string?> Avatars = new();
- // static readonly Dictionary<string, RemoteHomeserver> Servers = new();
+ [Parameter, SupplyParameterFromQuery]
+ public bool RenderEventInfo {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
- // private static List<StateEventResponse> PolicyEvents { get; set; } = new();
private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
- private StateEventResponse? CurrentlyEditingEvent {
- get => _currentlyEditingEvent;
+ public StateEventResponse? ServerPolicyToMakePermanent {
+ get;
set {
- _currentlyEditingEvent = value;
+ field = value;
StateHasChanged();
}
}
- // public bool EnableAvatars {
- // get => _enableAvatars;
- // set {
- // _enableAvatars = value;
- // if (value) GetAllAvatars();
- // }
- // }
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!;
+ private GenericRoom Room { get; set; } = null!;
+ private RoomPowerLevelEventContent PowerLevels { get; set; } = null!;
+ public bool CurrentUserIsDraupnir { get; set; }
- private AuthenticatedHomeserverGeneric Homeserver { get; set; }
- private GenericRoom Room { get; set; }
- private RoomPowerLevelEventContent PowerLevels { get; set; }
+ public Dictionary<StateEventResponse, int> ActiveKicks { get; set; } = [];
+
+ 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());
+
+ Dictionary<Type, PolicyCollection> PolicyCollections { get; set; } = new();
+ PolicyCollection? DuplicateBans { get; set; }
+ PolicyCollection? RedundantBans { get; set; }
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 LoadStatesAsync();
- Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
+ IsInitialised = true;
+ StateHasChanged();
+ await Task.WhenAll(
+ Task.Run(async () => { PowerLevels = (await Room.GetPowerLevelsAsync())!; }),
+ Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>(DraupnirProtectedRoomsData.EventId)) is not null; })
+ );
+ StateHasChanged();
+ await LoadStateAsync(firstLoad: true);
+ Loading = false;
+ logger.LogInformation("Policy list editor initialized in {SwElapsed}!", sw.Elapsed);
}
- private async Task LoadStatesAsync() {
+ private async Task LoadStateAsync(bool firstLoad = false) {
+ // preload workers in task pool
+ // await Task.WhenAll(Enumerable.Range(0, WebWorkerService.MaxWorkerCount).Select(async _ => (await WebWorkerService.TaskPool.GetWorkerAsync()).WhenReady).ToList());
+ var taskPoolReadyTask = WebWorkerService.TaskPool.SetWorkerCount(WebWorkerService.MaxWorkerCount);
+ var sw = Stopwatch.StartNew();
+ // Loading = true;
+ // var states = Room.GetFullStateAsync();
+ var states = await Room.GetFullStateAsListAsync();
+ // PolicyEventsByType.Clear();
+ logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed);
+
+ foreach (var type in KnownPolicyTypes) {
+ if (!PolicyCollections.ContainsKey(type)) {
+ var filterPropSw = Stopwatch.StartNew();
+ // 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 proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => props.Any(y => y.Name == x.Name))
+ .ToFrozenDictionary(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName(), x => x);
+ logger.LogInformation("{Count} proxy safe props found in {TypeFullName} ({TimeSpan})", proxySafeProps?.Count, type.FullName, filterPropSw.Elapsed);
+ PolicyCollections.Add(type, new() {
+ Name = type.GetFriendlyNamePluralOrNull() ?? type.FullName ?? type.Name,
+ ActivePolicies = [],
+ RemovedPolicies = [],
+ PropertiesToDisplay = proxySafeProps
+ });
+ }
+ }
+
+ var count = 0;
+ var parseSw = Stopwatch.StartNew();
+ foreach (var evt in states) {
+ var mappedType = evt.MappedType;
+ if (count % 100 == 0)
+ logger.LogInformation("Processing state #{Count:000000} {EvtType} @ {SwElapsed} (took {ParseSwElapsed:c} so far to process)", count, evt.Type, sw.Elapsed, parseSw.Elapsed);
+ count++;
+
+ if (!mappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
+
+ var collection = PolicyCollections[mappedType];
+
+ var key = (evt.Type, evt.StateKey!);
+ var policyInfo = new PolicyCollection.PolicyInfo {
+ Policy = evt,
+ MadeRedundantBy = [],
+ DuplicatedBy = []
+ };
+ if (evt.RawContent is null or { Count: 0 } || string.IsNullOrWhiteSpace(evt.RawContent?["recommendation"]?.GetValue<string>())) {
+ collection.ActivePolicies.Remove(key);
+ if (!collection.RemovedPolicies.TryAdd(key, policyInfo)) {
+ if (StateEvent.Equals(collection.RemovedPolicies[key].Policy, evt)) continue;
+ collection.RemovedPolicies[key] = policyInfo;
+ }
+ }
+ else {
+ collection.RemovedPolicies.Remove(key);
+ if (!collection.ActivePolicies.TryAdd(key, policyInfo)) {
+ if (StateEvent.Equals(collection.ActivePolicies[key].Policy, evt)) continue;
+ collection.ActivePolicies[key] = policyInfo;
+ }
+ }
+ }
+
+ logger.LogInformation("LoadStatesAsync: Processed state in {SwElapsed}", sw.Elapsed);
+ foreach (var collection in PolicyCollections) {
+ logger.LogInformation("Policy collection {KeyFullName} has {ActivePoliciesCount} active and {RemovedPoliciesCount} removed policies.", collection.Key.FullName, collection.Value.ActivePolicies.Count, collection.Value.RemovedPolicies.Count);
+ }
+
+ await Task.Delay(1);
+
+ Loading = false;
+ StateHasChanged();
+ await Task.Delay(100);
+
+ // return;
+ logger.LogInformation("LoadStatesAsync: Scanning for redundant policies...");
+
+ var scanSw = Stopwatch.StartNew();
+ // var allPolicyInfos = PolicyCollections.Values
+ // .SelectMany(x => x.ActivePolicies.Values)
+ // .ToArray();
+ // var allPolicies = allPolicyInfos
+ // .Select<PolicyCollection.PolicyInfo, (PolicyCollection.PolicyInfo PolicyInfo, PolicyRuleEventContent TypedContent)>(x => (x, (x.Policy.TypedContent as PolicyRuleEventContent)!))
+ // .ToList();
+ // var hashPolicies = allPolicies
+ // .Where(x => x.TypedContent.IsHashedRule())
+ // .ToList();
+ // var wildcardPolicies = allPolicies
+ // .Except(hashPolicies) // hashed policies cannot be wildcards
+ // .Where(x => x.TypedContent.IsGlobRule() || x.TypedContent is ServerPolicyRuleEventContent)
+ // .ToList();
+ // var nonWildcardPolicies = allPolicies
+ // // .Except(wildcardPolicies)
+ // .Where(x => !x.TypedContent!.IsGlobRule() || x.TypedContent is ServerPolicyRuleEventContent)
+ // .ToList();
+ // Console.WriteLine($"Got {allPolicies.Count} total policies, {wildcardPolicies.Count} wildcard policies. Time spent: {scanSw.Elapsed}");
+ // int i = 0;
+ // int hits = 0;
+ // int redundant = 0;
+ // int duplicates = 0;
+
+ // foreach (var (policyInfo, policyContent) in allPolicies) {
+ // foreach (var (otherPolicyInfo, otherPolicyContent) in allPolicies) {
+ // if (policyInfo.Policy == otherPolicyInfo.Policy) continue; // same event
+ // if (StateEvent.TypeKeyPairMatches(policyInfo.Policy, otherPolicyInfo.Policy)) {
+ // logger.LogWarning("Sanity check failed: Found same type and state key for two different policies: {Policy1} and {Policy2}", policyInfo.Policy.RawContent.ToJson(), otherPolicyInfo.Policy.RawContent.ToJson());
+ // continue; // same type and state key
+ // }
+ // // if(!policyContent.IsHashedRule())
+ // }
+ //
+ // if (++i % 100 == 0) {
+ // Console.WriteLine($"Processed {i} policies in {scanSw.Elapsed}");
+ // await Task.Delay(1);
+ // }
+ // }
+
+ int scanningPolicyCount = 0;
+ var aggregatedPolicies = PolicyCollections.Values
+ .Aggregate(new List<StateEventResponse>(), (acc, val) => {
+ acc.AddRange(val.ActivePolicies.Select(x => x.Value.Policy));
+ return acc;
+ });
+ Console.WriteLine($"Scanning for redundant policies in {aggregatedPolicies.Count} total policies... ({scanSw.Elapsed})");
+ List<Task<List<PolicyCollection.PolicyInfo>>> tasks = [];
+ // try to save some load...
+ var policiesJson = JsonSerializer.Serialize(aggregatedPolicies);
+ var policiesJsonMarshalled = JsRuntime.ReturnMe<SpawnDev.BlazorJS.JSObjects.String>(policiesJson);
+ var ranges = Enumerable.Range(0, aggregatedPolicies.Count).DistributeSequentially(WebWorkerService.MaxWorkerCount);
+ await taskPoolReadyTask;
+ tasks.AddRange(ranges.Select(range => WebWorkerService.TaskPool.Invoke(CheckDuplicatePoliciesAsync, policiesJsonMarshalled, range.First(), range.Last())));
+
+ Console.WriteLine($"Main: started {tasks.Count} workers in {scanSw.Elapsed}");
+ // tasks.Add(CheckDuplicatePoliciesAsync(allPolicyInfos, range.First() .. range.Last()));
+
+ // var allPolicyEvents = aggregatedPolicies.Select(x => x.Policy).ToList();
+
+ DuplicateBans = new() {
+ Name = "Duplicate bans",
+ ViewType = PolicyCollection.SpecialViewType.Duplicates,
+ ActivePolicies = [],
+ RemovedPolicies = [],
+ PropertiesToDisplay = PolicyCollections.SelectMany(x => x.Value.PropertiesToDisplay).DistinctBy(x => x.Key).ToFrozenDictionary()
+ };
+
+ RedundantBans = new() {
+ Name = "Redundant bans",
+ ViewType = PolicyCollection.SpecialViewType.Redundant,
+ ActivePolicies = [],
+ RemovedPolicies = [],
+ PropertiesToDisplay = PolicyCollections.SelectMany(x => x.Value.PropertiesToDisplay).DistinctBy(x => x.Key).ToFrozenDictionary()
+ };
+
+ var allPolicyInfos = PolicyCollections.Values
+ .SelectMany(x => x.ActivePolicies.Values)
+ .ToArray();
+
+ await foreach (var modifiedPolicyInfos in tasks.ToAsyncResultEnumerable()) {
+ if (modifiedPolicyInfos.Count == 0) continue;
+ var applySw = Stopwatch.StartNew();
+ // Console.WriteLine($"Main: got {modifiedPolicyInfos.Count} modified policies from worker, time: {scanSw.Elapsed}");
+ foreach (var modifiedPolicyInfo in modifiedPolicyInfos) {
+ var original = allPolicyInfos.First(p => p.Policy.EventId == modifiedPolicyInfo.Policy.EventId);
+ original.DuplicatedBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.DuplicatedBy.Any(y => StateEvent.Equals(x, y))).ToList();
+ original.MadeRedundantBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.MadeRedundantBy.Any(y => StateEvent.Equals(x, y))).ToList();
+ modifiedPolicyInfo.DuplicatedBy = modifiedPolicyInfo.MadeRedundantBy = []; // Early dereference
+ if (original.DuplicatedBy.Count > 0) {
+ if (!DuplicateBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!)))
+ DuplicateBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original);
+ }
+
+ if (original.MadeRedundantBy.Count > 0) {
+ if (!RedundantBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!)))
+ RedundantBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original);
+ }
+ // Console.WriteLine($"Memory usage: {Util.BytesToString(GC.GetTotalMemory(false))}");
+ }
+
+ Console.WriteLine($"Main: Processed {modifiedPolicyInfos.Count} modified policies in {scanSw.Elapsed} (applied in {applySw.Elapsed})");
+ }
+
+ Console.WriteLine($"Processed {allPolicyInfos.Length} policies in {scanSw.Elapsed}");
+
+ // // scan for wildcard matches
+ // foreach (var policy in allPolicies) {
+ // var matchingPolicies = wildcardPolicies
+ // .Where(x =>
+ // !StateEvent.TypeKeyPairMatches(policy.PolicyInfo.Policy, x.PolicyInfo.Policy)
+ // && x.Item2.EntityMatches(policy.TypedContent.Entity!)
+ // )
+ // .ToList();
+ //
+ // if (matchingPolicies.Count > 0) {
+ // logger.LogInformation($"{i} Got {matchingPolicies.Count} hits for {policy.PolicyInfo.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.PolicyInfo.Policy.RawContent).ToJson()}");
+ // foreach (var match in matchingPolicies) {
+ // policy.PolicyInfo.MadeRedundantBy.Add(match.PolicyInfo.Policy);
+ // }
+ //
+ // hits++;
+ // redundant += matchingPolicies.Count;
+ //
+ // if (hits % 5 == 0)
+ // StateHasChanged();
+ // }
+ // else {
+ // //logger.LogInformation("Sleeping...");
+ // await Task.Delay(1);
+ // }
+ //
+ // i++;
+ // }
+ //
+ // i = 0;
+ // // scan for exact duplicates
+ // foreach (var policy in allPolicies) {
+ // var matchingPolicies = allPolicies
+ // .Where(x =>
+ // !StateEvent.TypeKeyPairMatches(policy.PolicyInfo.Policy, x.PolicyInfo.Policy)
+ // && (
+ // x.Item2.IsHashedRule()
+ // ? x.Item2.EntityMatches(policy.Item2.Entity)
+ // : x.Item2!.Entity == policy.Item2.Entity!
+ // )
+ // )
+ // .ToList();
+ //
+ // if (matchingPolicies.Count > 0) {
+ // logger.LogInformation($"{i} Got {matchingPolicies.Count} duplicates for {policy.PolicyInfo.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.PolicyInfo.Policy.RawContent).ToJson()}");
+ // foreach (var match in matchingPolicies) {
+ // policy.PolicyInfo.MadeRedundantBy.Add(match.PolicyInfo.Policy);
+ // }
+ //
+ // hits++;
+ // duplicates += matchingPolicies.Count;
+ //
+ // if (hits % 5 == 0)
+ // StateHasChanged();
+ // }
+ // else {
+ // //logger.LogInformation("Sleeping...");
+ // await Task.Delay(1);
+ // }
+ //
+ // i++;
+ // }
+ //
+ // logger.LogInformation($"LoadStatesAsync: Found {hits} ({redundant} redundant, {duplicates} duplicates) redundant policies in {sw.Elapsed}");
+ // StateHasChanged();
+ }
+
+ [return: WorkerTransfer]
+ private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(SpawnDev.BlazorJS.JSObjects.String policiesJson, int start, int end) {
+ var policies = JsonSerializer.Deserialize<List<StateEventResponse>>(policiesJson.ValueOf());
+ Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.ValueOf().Length} bytes of JSON ({policies!.Count} policies)");
+ return await CheckDuplicatePoliciesAsync(policies!, start .. end);
+ }
+
+ [return: WorkerTransfer]
+ private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(string policiesJson, int start, int end) {
+ var policies = JsonSerializer.Deserialize<List<StateEventResponse>>(policiesJson);
+ Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.Length} bytes of JSON ({policies!.Count} policies)");
+ return await CheckDuplicatePoliciesAsync(policies!, start .. end);
+ }
+
+ [return: WorkerTransfer]
+ private static Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<StateEventResponse> policies, int start, int end)
+ => CheckDuplicatePoliciesAsync(policies, start .. end);
+
+ [return: WorkerTransfer]
+ private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<StateEventResponse> policies, Range range) {
+ var sw = Stopwatch.StartNew();
+ var jsConsole = App.Host.Services.GetService<JsConsoleService>()!;
+ Console.WriteLine($"Processing policies in range {range} ({range.GetOffsetAndLength(policies.Count).Length}) with {policies.Count} total policies");
+ var allPolicies = policies
+ .Select(x => (Event: x, TypedContent: (x.TypedContent as PolicyRuleEventContent)!))
+ .ToList();
+ var toCheck = allPolicies[range];
+ var modifiedPolicies = new List<PolicyCollection.PolicyInfo>();
+
+ foreach (var (policyEvent, policyContent) in toCheck) {
+ List<StateEventResponse> duplicatedBy = [];
+ List<StateEventResponse> madeRedundantBy = [];
+
+ foreach (var (otherPolicyEvent, otherPolicyContent) in allPolicies) {
+ if (policyEvent == otherPolicyEvent) continue; // same event
+ if (StateEvent.TypeKeyPairMatches(policyEvent, otherPolicyEvent)) {
+ // logger.LogWarning("Sanity check failed: Found same type and state key for two different policies: {Policy1} and {Policy2}", policyInfo.Policy.RawContent.ToJson(), otherPolicyInfo.Policy.RawContent.ToJson());
+ Console.WriteLine($"Sanity check failed: Found same type and state key for two different policies: {policyEvent.RawContent.ToJson()} and {otherPolicyEvent.RawContent.ToJson()}");
+ continue; // same type and state key
+ }
+
+ // if(!policyContent.IsHashedRule())
+ if (!string.IsNullOrWhiteSpace(policyContent.Entity) && policyContent.Entity == otherPolicyContent.Entity) {
+ // Console.WriteLine($"Found duplicate policy: {policyEvent.EventId} is duplicated by {otherPolicyEvent.EventId}");
+ duplicatedBy.Add(otherPolicyEvent);
+ }
+ }
+
+ if (duplicatedBy.Count > 0 || madeRedundantBy.Count > 0) {
+ var summary = $"Policy {policyEvent.EventId} is:";
+ if (duplicatedBy.Count > 0)
+ summary += $"\n- Duplicated by {duplicatedBy.Count} policies: {string.Join(", ", duplicatedBy.Select(x => x.EventId))}";
+ if (madeRedundantBy.Count > 0)
+ summary += $"\n- Made redundant by {madeRedundantBy.Count} policies: {string.Join(", ", madeRedundantBy.Select(x => x.EventId))}";
+ // Console.WriteLine(summary);
+ await jsConsole.Info(summary);
+ await Task.Delay(1);
+ modifiedPolicies.Add(new() {
+ Policy = policyEvent,
+ DuplicatedBy = duplicatedBy,
+ MadeRedundantBy = madeRedundantBy
+ });
+ }
+
+ // await Task.Delay(1);
+ }
+
+ await jsConsole.Info($"Worker: Found {modifiedPolicies.Count} modified policies in range {range} (length: {range.GetOffsetAndLength(policies.Count).Length}) in {sw.Elapsed}");
+
+ return modifiedPolicies;
+ }
+
+ // the old one:
+ private async Task LoadStatesAsync(bool firstLoad = false) {
+ await LoadStateAsync(firstLoad);
+ return;
+ var sw = Stopwatch.StartNew();
Loading = true;
- var states = Room.GetFullStateAsync();
- PolicyEventsByType.Clear();
- await foreach (var state in states) {
- if (state is null) continue;
+ // var states = Room.GetFullStateAsync();
+ var states = await Room.GetFullStateAsListAsync();
+ // PolicyEventsByType.Clear();
+
+ logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed);
+
+ foreach (var type in KnownPolicyTypes) {
+ if (!PolicyEventsByType.ContainsKey(type))
+ PolicyEventsByType.Add(type, new List
+ <StateEventResponse>(16000));
+ }
+
+ int count = 0;
+
+ foreach (var state in states) {
+ var _spsw = Stopwatch.StartNew();
+ TimeSpan e1, e2, e3, e4, e5, e6, t;
if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
- if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new());
- PolicyEventsByType[state.MappedType].Add(state);
+ e1 = _spsw.Elapsed;
+ var targetPolicies = PolicyEventsByType[state.MappedType];
+ e2 = _spsw.Elapsed;
+ if (!firstLoad && targetPolicies.FirstOrDefault(x => StateEvent.TypeKeyPairMatches(x, state)) is { } evt) {
+ e3 = _spsw.Elapsed;
+ if (StateEvent.Equals(evt, state)) {
+ if (count % 100 == 0) {
+ await Task.Delay(10);
+ await Task.Yield();
+ }
+
+ e4 = _spsw.Elapsed;
+ logger.LogInformation("[E] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={E3:c}, e4={E4:c}, e5={Zero:c},t={SpswElapsed:c})", count++, state.Type, sw.Elapsed, e1, e2, e3, e4, TimeSpan.Zero, _spsw.Elapsed);
+ continue;
+ }
+
+ e4 = _spsw.Elapsed;
+ targetPolicies.Remove(evt);
+ e5 = _spsw.Elapsed;
+ targetPolicies.Add(state);
+ e6 = _spsw.Elapsed;
+ t = _spsw.Elapsed;
+ logger.LogInformation("[M] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={E3:c}, e4={E4:c}, e5={E5:c}, e6={E6:c},t={TimeSpan1:c})", count++, state.Type, sw.Elapsed, e1, e2, e3, e4, e5, e6, t);
+ }
+ else {
+ targetPolicies.Add(state);
+ t = _spsw.Elapsed;
+ logger.LogInformation("[N] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={Zero:c}, e4={TimeSpan1:c}, e5={Zero1:c}, e6={TimeSpan2:c}, t={TimeSpan3:c})", count++, state.Type, sw.Elapsed, e1, e2, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, t);
+ }
+
+ // await Task.Delay(10);
+ // await Task.Yield();
}
+ logger.LogInformation("LoadStatesAsync: Processed state in {SwElapsed}", sw.Elapsed);
+
Loading = false;
StateHasChanged();
+ await Task.Delay(10);
+ await Task.Yield();
+ logger.LogInformation("LoadStatesAsync: yield finished in {SwElapsed}", sw.Elapsed);
}
- // 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();
-
- private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
- .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+ // 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 => x.RawContent is { Count: > 0 } && string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+ //
+ // private List<StateEventResponse> GetRemovedPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ // .Where(x => x.RawContent is null or { Count: 0 }).ToList();
private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
?? type.GetCustomAttributes<MatrixEventAttribute>()
@@ -242,27 +630,34 @@ else {
private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
- private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
- await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { });
- PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
- await LoadStatesAsync();
- }
+ public struct PolicyCollection {
+ public required string Name { get; init; }
+ public SpecialViewType ViewType { get; init; }
+ public int TotalCount => ActivePolicies.Count + RemovedPolicies.Count;
- private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
- await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent);
- CurrentlyEditingEvent = null;
- await LoadStatesAsync();
- }
+ public required Dictionary<(string Type, string StateKey), PolicyInfo> ActivePolicies { get; set; }
- private async Task UpgradePolicyAsync(StateEventResponse policyEvent) {
- policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type;
- await LoadStatesAsync();
- }
+ // public Dictionary<(string Type, string StateKey), StateEventResponse> InvalidPolicies { get; set; }
+ public required Dictionary<(string Type, string StateKey), PolicyInfo> RemovedPolicies { get; set; }
+ public required FrozenDictionary<string, PropertyInfo> PropertiesToDisplay { get; set; }
- private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+ public class PolicyInfo {
+ public required StateEventResponse Policy { get; init; }
+ public required List<StateEventResponse> MadeRedundantBy { get; set; }
+ public required List<StateEventResponse> DuplicatedBy { get; set; }
+ }
- // event types, unnamed
- private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
- .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+ public enum SpecialViewType {
+ None,
+ Duplicates,
+ Redundant,
+ }
+ }
+
+ // private struct PolicyStats {
+ // public int Active { get; set; }
+ // public int Invalid { get; set; }
+ // public int Removed { get; set; }
+ // }
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs
new file mode 100644
index 0000000..0106c6e
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs
@@ -0,0 +1,144 @@
+using LibMatrix;
+using LibMatrix.EventTypes.Interop.Draupnir;
+using LibMatrix.EventTypes.Spec.State.Policy;
+using LibMatrix.Homeservers;
+using LibMatrix.Services;
+using SpawnDev.BlazorJS.WebWorkers;
+
+namespace MatrixUtils.Web.Pages.Rooms;
+
+public partial class PolicyList {
+
+#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/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
new file mode 100644
index 0000000..5d5bb5d
--- /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 OnClickAsync="@(() => { 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 OnClickAsync="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton>
+ <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Remove</LinkButton>
+ @if (policy.IsLegacyType) {
+ <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
+ }
+
+ @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.EventId)) {
+ <LinkButton OnClickAsync="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent (wildcard)</LinkButton>
+ @if (CurrentUserIsDraupnir) {
+ <LinkButton OnClickAsync="@(() => 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/PolicyListComponents/PolicyListCategoryComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
new file mode 100644
index 0000000..932e0fe
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
@@ -0,0 +1,74 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.RoomTypes
+<details>
+ <summary>
+ <span>
+ @($"{PolicyCollection.Name}: {PolicyCollection.TotalCount} policies")
+ </span>
+ <hr style="margin: revert;"/>
+ </summary>
+ <table class="table table-striped table-hover table-bordered align-middle">
+ <thead>
+ <tr>
+ <th>Actions</th>
+ @foreach (var name in PolicyCollection.PropertiesToDisplay!.Keys) {
+ <th>@name</th>
+ }
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var policy in PolicyCollection.ActivePolicies.Values.OrderBy(x => x.Policy.RawContent?["entity"]?.GetValue<string>())) {
+ <PolicyListRowComponent PolicyCollectionStateHasChanged="@StateHasChanged" RenderEventInfo="RenderEventInfo" PolicyInfo="@policy" PolicyCollection="@PolicyCollection" Room="@Room"></PolicyListRowComponent>
+ }
+ </tbody>
+ </table>
+ @if (RenderInvalidSection) {
+ <details>
+ <summary>
+ <u>
+ @("Invalid " + PolicyCollection.Name.ToLower())
+ </u>
+ </summary>
+ <table class="table table-striped table-hover table-bordered align-middle">
+ <thead>
+ <tr>
+ <th>State key</th>
+ <th>Json contents</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var policy in PolicyCollection.RemovedPolicies.Values) {
+ <tr>
+ <td>@policy.Policy.StateKey</td>
+ <td>
+ <pre>@policy.Policy.RawContent.ToJson(true, false)</pre>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+ }
+</details>
+
+@code {
+
+ [Parameter]
+ public required PolicyList.PolicyCollection PolicyCollection { get; set; }
+
+ [Parameter]
+ public required GenericRoom Room { get; set; }
+
+ [Parameter]
+ public bool RenderEventInfo { get; set; }
+
+ [Parameter]
+ public bool RenderInvalidSection { get; set; } = true;
+
+ protected override bool ShouldRender() {
+ // if (PolicyCollection is null) return false;
+
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
new file mode 100644
index 0000000..8585561
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
@@ -0,0 +1,88 @@
+@using LibMatrix
+@using LibMatrix.EventTypes.Common
+@using LibMatrix.RoomTypes
+@using MatrixUtils.Web.Shared.PolicyEditorComponents
+<h3>Policy list editor - Editing @(RoomName ?? Room.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 OnClickAsync="@(() => {
+ CurrentlyEditingEvent = new() { Type = "", RawContent = new() };
+ return Task.CompletedTask;
+ })">Create new policy
+</LinkButton>
+<LinkButton OnClickAsync="@(() => {
+ MassCreatePolicies = true;
+ return Task.CompletedTask;
+ })">Create many new policies
+</LinkButton>
+<LinkButton OnClickAsync="@(() => ReloadStateAsync())">Refresh</LinkButton>
+
+@if (CurrentlyEditingEvent is not null) {
+ <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSaveAsync="@UpdatePolicyAsync"></PolicyEditorModal>
+}
+
+@if (MassCreatePolicies) {
+ <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => {
+ MassCreatePolicies = false;
+ // _ = LoadStatesAsync();
+ })"></MassPolicyEditorModal>
+}
+<br/>
+<InputCheckbox Value="@RenderEventInfo" ValueChanged="@RenderEventInfoChanged" ValueExpression="@(() => RenderEventInfo)"/>
+<span> Render event info</span>
+
+@code {
+
+ [Parameter]
+ public required GenericRoom Room { get; set; }
+
+ [Parameter]
+ public required Func<Task> ReloadStateAsync { get; set; }
+
+ [Parameter]
+ public required bool RenderEventInfo { get; set; }
+
+ [Parameter]
+ public required EventCallback<bool> RenderEventInfoChanged { get; set; }
+
+ private string? RoomName { get; set; }
+ private string? RoomAlias { get; set; }
+ private string? DraupnirShortcode { get; set; }
+
+ private StateEventResponse? CurrentlyEditingEvent {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private bool MassCreatePolicies {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ protected override async Task OnInitializedAsync() {
+ await Task.WhenAll(
+ 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(); })
+ );
+
+ StateHasChanged();
+ }
+
+ private async Task UpdatePolicyAsync(StateEventResponse evt) {
+ Console.WriteLine("UpdatePolicyAsync in PolicyListEditorHeader not yet implemented!");
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor
new file mode 100644
index 0000000..cd432c9
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor
@@ -0,0 +1,218 @@
+@using System.Reflection
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using LibMatrix.RoomTypes
+@using MatrixUtils.Web.Shared.PolicyEditorComponents
+
+@if (_isInitialized && IsVisible) {
+ <tr id="@PolicyInfo.Policy.EventId">
+ <td>
+ <div style="display: flex; flex-direction: row; gap: 0.5em;">
+ @* @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, Policy.Type)) { *@
+ @if (true) {
+ <LinkButton OnClickAsync="@(() => {
+ IsEditing = true;
+ return Task.CompletedTask;
+ })">Edit
+ </LinkButton>
+ <LinkButton OnClickAsync="@RemovePolicyAsync">Remove</LinkButton>
+ @if (Policy.IsLegacyType) {
+ <LinkButton OnClickAsync="@RemovePolicyAsync">Update type</LinkButton>
+ }
+
+ @if (TypedContent.Entity?.StartsWith("@*:", StringComparison.Ordinal) == true) {
+ <LinkButton OnClickAsync="@ConvertToAclAsync">Convert to ACL</LinkButton>
+ }
+
+ @* @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(Policy.Type)) { *@
+ @* <LinkButton OnClickAsync="@(() => { *@
+ @* ServerPolicyToMakePermanent = Policy; *@
+ @* return Task.CompletedTask; *@
+ @* })">Make permanent *@
+ @* </LinkButton> *@
+ @* @if (CurrentUserIsDraupnir) { *@
+ @* <LinkButton Color="@(ActiveKicks.ContainsKey(Policy) ? "#FF0000" : null)" OnClick="@(() => DraupnirKickMatching(Policy))">Kick *@
+ @* users @(ActiveKicks.TryGetValue(Policy, out var kick) ? $"({kick})" : null) *@
+ @* </LinkButton> *@
+ @* } *@
+ // }
+ }
+ else {
+ <p>No permission to modify</p>
+ }
+ </div>
+ </td>
+ @foreach (var prop in PolicyCollection.PropertiesToDisplay.Values) {
+ if (prop.Name == "Entity") {
+ <td>
+ <span>@TruncateMxid(TypedContent.Entity)</span>
+ @foreach (var dup in PolicyInfo.DuplicatedBy) {
+ <br/>
+ <span>Duplicated by @dup.FriendlyTypeName.ToLower() <a href="@Anchor(dup.EventId!)">@TruncateMxid(dup.RawContent["entity"]?.GetValue<string>())</a></span>
+ }
+ @foreach (var dup in PolicyInfo.MadeRedundantBy) {
+ <br/>
+ <span>Also matched by @dup.FriendlyTypeName.ToLower() <a href="@Anchor(dup.EventId!)">@TruncateMxid(dup.RawContent["entity"]?.GetValue<string>())</a></span>
+ }
+ @if (RenderEventInfo) {
+ <br/>
+ <pre style="margin-bottom: unset;">
+ @PolicyInfo.Policy.Type/@PolicyInfo.Policy.StateKey by @PolicyInfo.Policy.Sender at @PolicyInfo.Policy.OriginServerTimestamp
+ </pre>
+ }
+ </td>
+ }
+ else {
+ <td>@prop.GetGetMethod()?.Invoke(TypedContent, null)</td>
+ }
+ }
+ </tr>
+
+ @if (IsEditing) {
+ <PolicyEditorModal PolicyEvent="@Policy" OnClose="@(() => IsEditing = false)" OnSaveAsync="@UpdatePolicyAsync"></PolicyEditorModal>
+ }
+ @* TODO: Implement ability to turn ACLs into wildcards *@
+ @*@if (ServerPolicyToMakePermanent is not null) {
+ <ModalWindow Title="Make policy permanent">
+
+ </ModalWindow>
+ }*@
+}
+
+
+
+@code {
+
+ [Parameter]
+ public PolicyList.PolicyCollection.PolicyInfo PolicyInfo { get; set; }
+
+ [Parameter]
+ public GenericRoom Room { get; set; } = null!;
+
+ [Parameter]
+ public required PolicyList.PolicyCollection PolicyCollection { get; set; }
+
+ [Parameter]
+ public bool RenderEventInfo { get; set; }
+
+ [Parameter]
+ public required Action PolicyCollectionStateHasChanged { get; set; }
+
+ private StateEventResponse Policy => PolicyInfo.Policy;
+
+ private bool IsEditing {
+ get;
+ set {
+ field = value;
+ _isDirty = true;
+ StateHasChanged();
+ }
+ }
+
+ public bool IsVisible {
+ get;
+ set {
+ field = value;
+ _isDirty = true;
+ }
+ } = true;
+
+ private PolicyRuleEventContent TypedContent { get; set; }
+
+ private bool _isDirty = true;
+ private bool _isInitialized;
+
+ protected override bool ShouldRender() => _isDirty;
+
+ protected override void OnParametersSet() {
+ TypedContent = Policy.TypedContent as PolicyRuleEventContent ?? throw new InvalidOperationException("Policy must have a typed content of type PolicyRuleEventContent.");
+ _isDirty = true;
+ _isInitialized = true;
+ // Console.WriteLine($"ParametersSet {Policy.StateKey}");
+ }
+
+ private static string TruncateMxid(string? mxid) {
+ if (string.IsNullOrWhiteSpace(mxid)) return mxid;
+ var parts = mxid.Split(':', 2);
+ if (parts[0].Length > 50)
+ parts[0] = parts[0][..50] + "[...]";
+
+ if (parts is [_, { Length: > 50 }])
+ parts[1] = parts[1][..50] + "[...]";
+
+ return parts.Length == 1 ? parts[0] : $"{parts[0]}:{parts[1]}";
+ }
+
+ private async Task RemovePolicyAsync() {
+ await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, new { });
+ bool shouldUpdateVisibility = true;
+ PolicyCollection.ActivePolicies.Remove((Policy.Type, Policy.StateKey));
+ PolicyCollection.RemovedPolicies.Add((Policy.Type, Policy.StateKey), PolicyInfo);
+ if (PolicyInfo.DuplicatedBy.Count > 0) {
+ foreach (var evt in PolicyInfo.DuplicatedBy) {
+ var matchingEntry = PolicyCollection.ActivePolicies
+ .FirstOrDefault(x => StateEvent.Equals(x.Value.Policy, evt)).Value;
+ var removals = matchingEntry.DuplicatedBy.RemoveAll(x => StateEvent.Equals(x, Policy));
+ Console.WriteLine($"Removed {removals} duplicates from {evt.EventId}, matching entry: {matchingEntry.ToJson()}");
+ if (PolicyCollection.ViewType == PolicyList.PolicyCollection.SpecialViewType.Duplicates && matchingEntry.DuplicatedBy.Count == 0) {
+ PolicyCollection.ActivePolicies.Remove((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey));
+ PolicyCollection.RemovedPolicies.Add((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey), matchingEntry);
+ Console.WriteLine($"Also removed {matchingEntry.Policy.EventId} as it is now redundant");
+ }
+ }
+
+ PolicyCollectionStateHasChanged();
+ shouldUpdateVisibility = false;
+ }
+
+ if (PolicyInfo.MadeRedundantBy.Count > 0) {
+ foreach (var evt in PolicyInfo.MadeRedundantBy) {
+ var matchingEntry = PolicyCollection.ActivePolicies
+ .FirstOrDefault(x => StateEvent.Equals(x.Value.Policy, evt)).Value;
+ var removals = matchingEntry.MadeRedundantBy.RemoveAll(x => StateEvent.Equals(x, Policy));
+ Console.WriteLine($"Removed {removals} redundants from {evt.EventId}, matching entry: {matchingEntry.ToJson()}");
+ }
+
+ PolicyCollectionStateHasChanged();
+ shouldUpdateVisibility = false;
+ }
+
+ if (shouldUpdateVisibility) {
+ IsVisible = false;
+ StateHasChanged();
+ }
+ // PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
+ // await LoadStatesAsync();
+ }
+
+ private async Task UpdatePolicyAsync(StateEventResponse evt) {
+ await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, Policy.RawContent);
+ // CurrentlyEditingEvent = null;
+ // await LoadStatesAsync();
+ }
+
+ private async Task UpgradePolicyAsync() {
+ Policy.RawContent["gay.rory.matrixutils.upgraded_from_type"] = Policy.Type;
+ // await LoadStatesAsync();
+ }
+
+ private async Task ConvertToAclAsync() {
+ if (Policy.RawContent.ContainsKey("entity")) {
+ var newContent = Policy.ContentAs<ServerPolicyRuleEventContent>();
+ newContent!.Entity = newContent.Entity!.Replace("@*:", "");
+ await Room.SendStateEventAsync(ServerPolicyRuleEventContent.EventId, newContent.GetDraupnir2StateKey(), newContent);
+ await Room.SendStateEventAsync(Policy.Type, Policy.StateKey!, new { });
+ IsVisible = false;
+ StateHasChanged();
+ }
+ else {
+ throw new InvalidOperationException("Policy event must contain an 'entity' field to convert to ACL.");
+ }
+ }
+
+ private string Anchor(string anchor) {
+ return $"{NavigationManager.Uri.Split('#')[0]}#{anchor}";
+ }
+
+}
\ 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..52c5f30
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
@@ -0,0 +1,238 @@
+@page "/PolicyLists"
+@using ArcaneLibs
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes
+@using LibMatrix.EventTypes.Common
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using LibMatrix.Helpers
+@using LibMatrix.Responses
+@using LibMatrix.RoomTypes
+@inject ILogger<Index> logger
+<h3>
+ <span>Policy lists </span>
+ <LinkButton OnClickAsync="@(() => {
+ ShowPolicyListCreationWindow = true;
+ return Task.CompletedTask;
+ })">
+ <span class="oi oi-plus" aria-hidden="true"> Create</span>
+ </LinkButton>
+</h3>
+
+
+@if (!string.IsNullOrWhiteSpace(Status)) {
+ <p>@Status</p>
+}
+@if (!string.IsNullOrWhiteSpace(Status2)) {
+ <p>@Status2</p>
+}
+<hr/>
+
+<table class="table table-striped table-hover table-bordered align-middle" aria-busy="@isLoading">
+ <thead>
+ <tr>
+ <th>Room name</th>
+ <th>Policies</th>
+ <th/>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var room in Rooms.OrderByDescending(x => x.PolicyCounts.Sum(y => y.Value))) {
+ <tr>
+ <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>
+ <td>
+ <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")">
+ <span class="oi oi-pencil" aria-hidden="true"> View/edit policies</span>
+ </LinkButton>
+ </td>
+ </tr>
+ }
+ </tbody>
+</table>
+
+@if (ShowPolicyListCreationWindow && Homeserver != null) {
+ <ModalWindow Title="New policy list">
+ @if (!string.IsNullOrWhiteSpace(_roomBuilder.Avatar.Url)) {
+ <MxcAvatar Homeserver="@Homeserver" MxcUri="@_roomBuilder.Avatar.Url" Circular="true" Size="4" SizeUnit="em"/>
+ }
+ else {
+ <img class="avatar" style="height: 4em; width: 4em; border-radius: 50%;" src="@IdenticonGenerator.GenerateAsDataUri(Homeserver.WhoAmI.UserId)"/>
+ }
+ <div style="display: inline-block; vertical-align: middle; padding-left: 1em;">
+ <FancyTextBox @bind-Value="@_roomBuilder.Name.Name"></FancyTextBox>
+ <br/>
+ <span>#</span>
+ <FancyTextBox @bind-Value="@_roomBuilder.AliasLocalPart"></FancyTextBox>
+ <span>:@Homeserver!.ServerName</span>
+ <br/>
+ <FancyTextBox @bind-Value="@_roomBuilder.Avatar.Url"></FancyTextBox>
+ <InputFile OnChange="@RoomIconFilePicked"></InputFile>
+ </div>
+ <br/>
+
+ <span>Bot shortcode: </span>
+ <FancyTextBox @bind-Value="@_shortcodeEvent.Shortcode"></FancyTextBox>
+ <br/>
+ <LinkButton OnClickAsync="@CreatePolicyList">Create</LinkButton>
+
+ </ModalWindow>
+}
+
+@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;
+
+ isLoading = true;
+ Status = "Fetching rooms...";
+ List<Task> _tasks = [];
+ await foreach (var room in Homeserver.GetJoinedRoomsByType("support.feline.policy.lists.msc.v1")) {
+ // roomsByType.Add(room);
+ Status2 = $"Found {room.RoomId} (MSC3784)...";
+ _tasks.Add(Task.Run(async () => {
+ Rooms.Add(await RoomInfo.FromRoom(room));
+ StateHasChanged();
+ }));
+ }
+
+ await Task.WhenAll(_tasks);
+
+ isLoading = false;
+ Status = "";
+ Status2 = "";
+ }
+
+ private async Task ScanLegacyLists() {
+ isLoading = true;
+ 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 => PolicyRoom.SpecPolicyEventTypes.Contains(x.Type))
+ .ToList();
+ if (policies.Count == 0) return null;
+ Status2 = $"Found legacy list {room.RoomId}...";
+ return await RoomInfo.FromRoom(room, state, true);
+ }).ToAsyncResultEnumerable();
+
+ await foreach (var room in rooms) {
+ if (room is not null) {
+ Rooms.Add(room);
+ StateHasChanged();
+ }
+ }
+
+ isLoading = false;
+ Status = "";
+ Status2 = "";
+ }
+
+ private string? Status {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private string? Status2 {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private bool ShowPolicyListCreationWindow {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ } = true;
+
+ 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
+ }
+
+ 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 => PolicyRoom.UserPolicyEventTypes.Contains(x.Type)) },
+ { PolicyType.Server, state.Count(x => PolicyRoom.ServerPolicyEventTypes.Contains(x.Type)) },
+ { PolicyType.Room, state.Count(x => PolicyRoom.RoomPolicyEventTypes.Contains(x.Type)) }
+ }
+ };
+ }
+ }
+
+ private readonly RoomBuilder _roomBuilder = new() {
+ Type = "support.feline.policy.lists.msc.v1",
+ Name = new() { Name = "New policy list" },
+ AliasLocalPart = "policies"
+ };
+
+ private readonly MjolnirShortcodeEventContent _shortcodeEvent = new() {
+ Shortcode = "policy-list"
+ };
+
+ private bool isLoading = true;
+
+ private static readonly SvgIdenticonGenerator IdenticonGenerator = new();
+
+ private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) {
+ var res = await Homeserver!.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType);
+ Console.WriteLine(res);
+ _roomBuilder.Avatar.Url = res;
+ StateHasChanged();
+ }
+
+ private async Task CreatePolicyList() {
+ var room = await _roomBuilder.Create(Homeserver!);
+ Status = $"Created policy list {room.RoomId} ({room.GetNameAsync()})";
+ await room.SendStateEventAsync(MjolnirShortcodeEventContent.EventId, _shortcodeEvent);
+ NavigationManager.NavigateTo($"/Rooms/{room.RoomId}/Policies");
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor
new file mode 100644
index 0000000..c1ee202
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor
@@ -0,0 +1,52 @@
+@using ArcaneLibs
+@using LibMatrix.Helpers
+<tr>
+ <td>Room name:</td>
+ <td>
+ <FancyTextBox @bind-Value="@roomBuilder.Name.Name"></FancyTextBox>
+ </td>
+</tr>
+<tr>
+ <td>Room alias:</td>
+ <td>
+ <InputLocalPart Sigil="#" ServerName="@Homeserver.ServerName" @bind-LocalPart="@roomBuilder.AliasLocalPart"></InputLocalPart>
+ </td>
+</tr>
+<tr>
+ <td>Room icon:</td>
+ <td>
+ @if (!string.IsNullOrWhiteSpace(roomBuilder.Avatar.Url)) {
+ <MxcAvatar Homeserver="Homeserver" MxcUri="@roomBuilder.Avatar.Url" Size="3" SizeUnit="em" Circular="true"/>
+ }
+ else {
+ <img class="avatar" style="height: 3em; width: 3em; border-radius: 50%;" src="@IdenticonGenerator.GenerateAsDataUri(Homeserver.WhoAmI.UserId)"/>
+ }
+ <div style="display: inline-block; vertical-align: middle;">
+ <FancyTextBox @bind-Value="@roomBuilder.Avatar.Url"></FancyTextBox>
+ <br/>
+ <SimpleFilePicker OnFilePicked="@RoomIconFilePicked"></SimpleFilePicker>
+ </div>
+ </td>
+</tr>
+
+@code {
+
+ [Parameter]
+ public required RoomBuilder roomBuilder { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+ private static readonly SvgIdenticonGenerator IdenticonGenerator = new();
+
+ private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) {
+ var res = await Homeserver.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType);
+ Console.WriteLine(res);
+ roomBuilder.Avatar.Url = res;
+ PageStateHasChanged();
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor
new file mode 100644
index 0000000..3f4a73d
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor
@@ -0,0 +1,92 @@
+@using Blazored.LocalStorage
+@using LibMatrix.Helpers
+@inject ILocalStorageService LocalStorage
+<tr>
+ <td>Room type:</td>
+ <td>
+ @if (RoomTypes.ContainsKey(roomBuilder.Type ?? "")) {
+ <InputSelect @bind-Value="@roomBuilder.Type">
+ @foreach (var type in RoomTypes) {
+ <option value="@type.Key">@type.Value</option>
+ }
+ <option value="custom">Custom ...</option>
+ </InputSelect>
+ }
+ else {
+ <FancyTextBox @bind-Value="@roomBuilder.Type"></FancyTextBox>
+ }
+
+ <span> version </span>
+ @if (Capabilities is null) {
+ <span style="color: #888;">Loading...</span>
+ }
+ else {
+ <InputSelect @bind-Value="@roomBuilder.Version">
+ @foreach (var version in Capabilities.Capabilities.RoomVersions!.Available!) {
+ <option value="@version.Key">@version.Key (@version.Value)</option>
+ }
+ </InputSelect>
+ }
+ </td>
+</tr>
+<tr>
+ <td style="vertical-align: top;">Allow attribution:</td>
+ <td>
+ <InputCheckbox @bind-Value="@AllowAttribution"/>
+ <span>Allow attribution to Rory&::MatrixUtils</span>
+ <LinkButton InlineText="true" OnClick="@(() => ShowAttributionInfo = true)">?</LinkButton>
+ </td>
+</tr>
+
+@if (ShowAttributionInfo) {
+ <ModalWindow Title="Allow attribution to Rory&::MatrixUtils"
+ OnCloseClicked="@(() => ShowAttributionInfo = false)">
+ <span>This will add the following to the room creation content:</span>
+ <br/>
+ <pre>{ "gay.rory.created_using": "Rory&::MatrixUtils (https://mru.rory.gay)" }</pre>
+ <span>This is not visible to users unless they manually inspect the room's create event source.</span>
+ </ModalWindow>
+}
+
+@code {
+
+ [Parameter]
+ public required RoomBuilder roomBuilder { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+ private AuthenticatedHomeserverGeneric.CapabilitiesResponse? Capabilities { get; set; }
+
+ private bool ShowAttributionInfo {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private bool AllowAttribution {
+ get;
+ set {
+ field = value;
+ _ = LocalStorage.SetItemAsync("rmu.room_create.allow_attribution", value);
+ }
+ } = true;
+
+ protected override async Task OnInitializedAsync() {
+ Capabilities = await Homeserver.GetCapabilitiesAsync();
+ roomBuilder.Version = Capabilities.Capabilities.RoomVersions!.Default;
+ AllowAttribution = await LocalStorage.GetItemAsync<bool?>("rmu.room_create.allow_attribution") ?? true;
+ }
+
+ private static Dictionary<string, string> RoomTypes { get; } = new() {
+ { "", "Room" },
+ { "m.space", "Space" },
+ { "support.feline.policy.lists.msc.v1", "Policy list" }
+ };
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor
new file mode 100644
index 0000000..99facbf
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor
@@ -0,0 +1,83 @@
+@using System.Text.Json
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.Helpers
+<tr>
+ <td style="vertical-align: top;">Initial room state:</td>
+ <td>
+ @foreach (var (displayName, events) in new Dictionary<string, List<StateEvent>>() {
+ { "Important room state (before final access rules)", roomBuilder.ImportantState },
+ { "Additional room state (after final access rules)", roomBuilder.InitialState },
+ }) {
+ <details open>
+
+ @code
+ {
+ // private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" };
+ }
+
+ @* <summary>@displayName: @events.Count(x => !ImplementedStates.Contains(x.Type)) events</summary> *@
+ <summary>@displayName: @events.Count events</summary>
+ <LinkButton OnClick="@(() => {
+ events.Clear();
+ StateHasChanged();
+ })">Remove all
+ </LinkButton>
+ <LinkButton OnClick="@(() => {
+ events.Insert(0, new() {
+ Type = "",
+ StateKey = "",
+ RawContent = new(),
+ });
+ StateHasChanged();
+ })">Add new event
+ </LinkButton>
+ <br/>
+ @if (events.Count > 1000) {
+ <span style="color: red;">Warning: Too many initial state events! (more than 1000) - Please use the save/load feature in the state panel instead.</span>
+ }
+ else {
+ int i = 0;
+ @foreach (var initialState in events) {
+ <div id="@(initialState.Type + "/" + initialState.StateKey)">
+ <span>Event @(++i) (@GetRemoveButton(events, initialState))</span>
+ <br/>
+ @* <FancyTextBox Multiline="true" Value="@initialState.ToJson(ignoreNull: true)" *@
+ @* ValueChanged="@(json => { *@
+ @* if (string.IsNullOrWhiteSpace(json)) *@
+ @* events.Remove(initialState); *@
+ @* else *@
+ @* events.Replace(initialState, JsonSerializer.Deserialize<StateEvent>(json)); *@
+ @* StateHasChanged(); *@
+ @* })"></FancyTextBox> *@
+ <FancyTextBoxLazyJson T="StateEvent" Value="@initialState" ValueChanged="@(evt => { events.Replace(initialState, evt); })"></FancyTextBoxLazyJson>
+ <br/>
+ </div>
+ }
+ }
+ </details>
+ }
+ </td>
+</tr>
+
+@code {
+
+ [Parameter]
+ public required RoomBuilder roomBuilder { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+ private RenderFragment GetRemoveButton(List<StateEvent> events, StateEvent initialState) {
+ return @<span>
+ <LinkButton InlineText="true" OnClick="@(() => {
+ events.Remove(initialState);
+ PageStateHasChanged();
+ })">Remove</LinkButton>
+ </span>;
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor
new file mode 100644
index 0000000..6e300d4
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor
@@ -0,0 +1,60 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.Helpers
+<tr>
+ <td>Invited members:</td>
+ <td>
+ <details>
+ <summary>@roomBuilder.Invites.Count members</summary>
+ <LinkButton OnClickAsync="@InviteAllSessions" InlineText="true">Invite all logged in accounts</LinkButton>
+ <br/>
+ @foreach (var member in roomBuilder.Invites) {
+ <FancyTextBox Value="@member.Key" ValueChanged="@(val => roomBuilder.Invites.ChangeKey(member.Key, val))"/>
+ @* <UserListItem _homeserver="Homeserver" UserId="@member.Key"></UserListItem> *@
+ <span>: </span>
+ <FancyTextBox Value="@member.Value" ValueChanged="@(val => roomBuilder.Invites[member.Key] = val)"/>
+ <br/>
+ }
+ </details>
+ </td>
+</tr>
+<tr>
+ <td>Banned members:</td>
+ <td>
+ <details>
+ <summary>@roomBuilder.Bans.Count members</summary>
+ <br/>
+ @foreach (var member in roomBuilder.Bans) {
+ @* <UserListItem _homeserver="Homeserver" UserId="@member.Key"></UserListItem> *@
+ <FancyTextBox Value="@member.Value" ValueChanged="@(val => roomBuilder.Bans.ChangeKey(member.Key, val))"/>
+ <span>: </span>
+ <FancyTextBox Value="@member.Value" ValueChanged="@(val => roomBuilder.Bans[member.Key] = val)"/>
+ }
+ </details>
+ </td>
+</tr>
+
+@code {
+
+ [Parameter]
+ public required RoomBuilder roomBuilder { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+ private async Task InviteAllSessions() {
+ var sessions = await sessionStore.GetAllSessions();
+ foreach (var session in sessions) {
+ if (roomBuilder.Invites.ContainsKey(session.Value.Auth.UserId) || session.Value.Auth.UserId == Homeserver!.WhoAmI.UserId) continue;
+ Console.WriteLine("Inviting " + session.Value.Auth.UserId);
+ roomBuilder.Invites.Add(session.Value.Auth.UserId, null);
+ Console.WriteLine("--");
+ }
+
+ Console.WriteLine("Got all sessions, invited: " + string.Join(", ", roomBuilder.Invites.Keys));
+ StateHasChanged();
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor
new file mode 100644
index 0000000..94e9638
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor
@@ -0,0 +1,19 @@
+@using LibMatrix.Helpers
+<div style="border-left: solid 1px white; padding-left: 8px; margin-left: 8px;">
+ <span>Policy list upgrade type:</span>
+ <InputSelect @bind-Value="@roomUpgrade.UpgradeOptions.Msc4321PolicyListUpgradeOptions.UpgradeType">
+ <option value="@RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Move">Move policy list (copy policies)</option>
+ <option value="@RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Transition">Transition policy list (new list)</option>
+ </InputSelect>
+ <br/>
+</div>
+
+@code {
+
+ [Parameter]
+ public required RoomUpgradeBuilder roomUpgrade { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor
new file mode 100644
index 0000000..ba28b82
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor
@@ -0,0 +1,123 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.Helpers
+<tr>
+ <td>Permissions:</td>
+ <details>
+ <summary>
+ @if (roomBuilder.Version is "org.matrix.hydra.11" or "12") {
+ <span>@(roomBuilder.AdditionalCreators.Count + 1) creators, </span>
+ }
+ <span>@roomBuilder.PowerLevels.Users.Count members, @roomBuilder.PowerLevels.Events.Count events</span>
+ </summary>
+
+ @if (roomBuilder.Version is "org.matrix.hydra.11" or "12") {
+ <span style="border-bottom: #444;">Creators:</span>
+ <br/>
+ <span>@Homeserver.WhoAmI.UserId (you - to change, visit <a href="/">the homepage</a>.)</span>
+ <br/>
+
+ <StringListEditor @bind-Items="@roomBuilder.AdditionalCreators"></StringListEditor>
+ <br/>
+ }
+
+ <span style="border-bottom: #444;">Events:</span><br/>
+ @foreach (var eventType in roomBuilder.PowerLevels.Events.Keys) {
+ var _event = eventType;
+ <tr>
+ <td>
+ <LinkButton InlineText="true" OnClick="@(() => {
+ roomBuilder.PowerLevels.Events.Remove(_event);
+ StateHasChanged();
+ })">-
+ </LinkButton>
+ <div style="display: inline-flex;">
+ <FancyTextBox Formatter="@GetPermissionFriendlyName"
+ Value="@_event"
+ ValueChanged="val => { roomBuilder.PowerLevels.Events.ChangeKey(_event, val); }">
+ </FancyTextBox>
+ <span>:</span>
+ </div>
+ </td>
+ <td>
+ <input type="number" value="@roomBuilder.PowerLevels.Events[_event]"
+ @oninput="val => { roomBuilder.PowerLevels.Events[_event] = int.Parse(val.Value.ToString()); }"
+ @onfocusout="@(() => { roomBuilder.PowerLevels.Events = roomBuilder.PowerLevels.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); })"/>
+ </td>
+ </tr>
+ }
+ <tr>
+ <td>
+ <LinkButton InlineText="true" OnClick="@(() => {
+ roomBuilder.PowerLevels.Events[""] = 0;
+ StateHasChanged();
+ })">+
+ </LinkButton>
+ </td>
+ </tr>
+
+ <span style="border-bottom: #444;">Users:</span><br/>
+ @foreach (var user in roomBuilder.PowerLevels.Users.Keys) {
+ var _user = user;
+ <tr>
+ <td>
+ <LinkButton InlineText="true" OnClick="@(() => {
+ roomBuilder.PowerLevels.Users.Remove(_user);
+ StateHasChanged();
+ })">-
+ </LinkButton>
+ <div style="display: inline-flex;">
+ <FancyTextBox Value="@_user"
+ ValueChanged="val => { roomBuilder.PowerLevels.Users.ChangeKey(_user, val); }">
+ </FancyTextBox>
+ <span>:</span>
+ </div>
+ </td>
+ <td>
+ <input type="number" value="@roomBuilder.PowerLevels.Users[_user]"
+ @oninput="val => { roomBuilder.PowerLevels.Users[_user] = int.Parse(val.Value.ToString()); }"
+ @onfocusout="@(() => { roomBuilder.PowerLevels.Users = roomBuilder.PowerLevels.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); })"/>
+ </td>
+ </tr>
+ }
+ <tr>
+ <td>
+ <LinkButton InlineText="true" OnClick="@(() => {
+ roomBuilder.PowerLevels.Users[""] = 0;
+ StateHasChanged();
+ })">+
+ </LinkButton>
+ </td>
+ </tr>
+ </details>
+</tr>
+
+
+@code {
+
+ [Parameter]
+ public required RoomBuilder roomBuilder { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+ private string GetPermissionFriendlyName(string key) => key switch {
+ "m.reaction" => "Send reaction",
+ "m.room.avatar" => "Change room icon",
+ "m.room.canonical_alias" => "Change room alias",
+ "m.room.encryption" => "Enable encryption",
+ "m.room.history_visibility" => "Change history visibility",
+ "m.room.name" => "Change room name",
+ "m.room.power_levels" => "Change power levels",
+ "m.room.tombstone" => "Upgrade room",
+ "m.room.topic" => "Change room topic",
+ "m.room.pinned_events" => "Pin events",
+ "m.room.server_acl" => "Change server ACLs",
+ "org.matrix.msc4284.policy" => "Change policy server",
+ "m.room.guest_access" => "Change guest access",
+ _ => key
+ };
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor
new file mode 100644
index 0000000..76f61c4
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor
@@ -0,0 +1,70 @@
+@using LibMatrix.Helpers
+<tr>
+ <td style="padding-top: 16px;">Join rules:</td>
+ <td style="padding-top: 16px;">
+ <InputSelect @bind-Value="@roomBuilder.JoinRules.JoinRuleValue">
+ <option value="public">Anyone can join</option>
+ <option value="invite">Invite only</option>
+ <option value="knock">Ask to join</option>
+ <option value="restricted">Invite only (or mutual room)</option>
+ <option value="knock_restricted">Ask to join (or mutual room)</option>
+ </InputSelect>
+ </td>
+</tr>
+<tr>
+ <td>History visibility:</td>
+ <td>
+ <InputSelect @bind-Value="@roomBuilder.HistoryVisibility.HistoryVisibility">
+ <option value="invited">Since invite</option>
+ <option value="joined">Since join</option>
+ <option value="shared">Since room creation (members only)</option>
+ <option value="world_readable">World readable (everyone)</option>
+ </InputSelect>
+ </td>
+</tr>
+<tr>
+ <td>Guest access:</td>
+ <td>
+ <InputCheckbox @bind-Value="roomBuilder.GuestAccess.IsGuestAccessEnabled"/>
+ <span>Allow guests to join</span>
+ <LinkButton InlineText="true" href="https://spec.matrix.org/v1.15/client-server-api/#guest-access" target="_blank">?</LinkButton>
+ </td>
+</tr>
+<tr>
+ <td>Server ACLs:</td>
+ <td>
+ @if (roomBuilder.ServerAcls?.Allow is null) {
+ <p>No allow rules exist!</p>
+ <LinkButton OnClick="@(() => { roomBuilder.ServerAcls!.Allow = ["*"]; })">Create sane defaults</LinkButton>
+ }
+ else {
+ <details>
+ <summary>@(roomBuilder.ServerAcls.Allow?.Count) allow rules</summary>
+ <StringListEditor @bind-Items="@roomBuilder.ServerAcls.Allow"></StringListEditor>
+ </details>
+ }
+ @if (roomBuilder.ServerAcls?.Deny is null) {
+ <p>No deny rules exist!</p>
+ <LinkButton OnClick="@(() => { roomBuilder.ServerAcls!.Deny = []; })">Create sane defaults</LinkButton>
+ }
+ else {
+ <details>
+ <summary>@(roomBuilder.ServerAcls.Deny?.Count) deny rules</summary>
+ <StringListEditor @bind-Items="@roomBuilder.ServerAcls.Deny"></StringListEditor>
+ </details>
+ }
+ </td>
+</tr>
+
+@code {
+
+ [Parameter]
+ public required RoomBuilder roomBuilder { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor
new file mode 100644
index 0000000..eb373ba
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor
@@ -0,0 +1,65 @@
+@using System.Text.Json
+@using System.Text.Json.Nodes
+@using ArcaneLibs.Blazor.Components.Services
+@using ArcaneLibs.Extensions
+@using LibMatrix.Helpers
+@inject BlazorSaveFileService SaveFileService
+<div
+ style="position: fixed; top: 56px; right: 0; width: fit-content; max-width: 25%; height: calc(100vh - 56px); overflow: auto; background-color: #2c3054; padding-right: 32px; border-left: 1px solid #ccc;">
+ <details open>
+ <summary>RoomBuilder state</summary>
+ <InputCheckbox @bind-Value="@ShowNullInState"/>
+ <span>Show null values</span><br/>
+ <LinkButton OnClickAsync="@SaveFile">Save</LinkButton>
+ <SimpleFilePicker OnFilePicked="@LoadFile"/>
+ <br/>
+ <pre>
+ @RoomBuilder.ToJson(ignoreNull: !ShowNullInState)
+ </pre>
+ </details>
+</div>
+
+@code {
+
+ [Parameter]
+ public required RoomBuilder RoomBuilder { get; set; }
+
+ [Parameter]
+ public required EventCallback<RoomBuilder> RoomBuilderChanged { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+ private bool ShowNullInState { get; set; }
+
+ private async Task SaveFile() {
+ Console.WriteLine("Saving room builder state to file...");
+ await SaveFileService.SaveFileAsync("room-builder.json", RoomBuilder.ToJson(), "application/json");
+ }
+
+ private async Task LoadFile(InputFileChangeEventArgs e) {
+ if (!RoomBuilderChanged.HasDelegate) throw new InvalidOperationException("RoomBuilderChanged must have a delegate.");
+ if (e.FileCount == 0) return;
+ Console.WriteLine("Loading room builder state from file...");
+ var stream = e.File.OpenReadStream(4 * 1024 * 1024 * 1024L);
+ var json = await JsonSerializer.DeserializeAsync<JsonObject>(stream);
+ if (json is null) {
+ Console.WriteLine("Failed to deserialize JSON from file.");
+ return;
+ }
+
+ if (json.ContainsKey(nameof(RoomUpgradeBuilder.UpgradeOptions))) {
+ Console.WriteLine("Got room upgrade builder state.");
+ RoomBuilder = json.Deserialize<RoomUpgradeBuilder>();
+ }
+ else {
+ Console.WriteLine("Got room builder state.");
+ RoomBuilder = json.Deserialize<RoomBuilder>();
+ }
+
+ await RoomBuilderChanged.InvokeAsync(RoomBuilder);
+ PageStateHasChanged();
+ Console.WriteLine("Room builder state loaded from file.");
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor
new file mode 100644
index 0000000..d4c4bfe
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor
@@ -0,0 +1,51 @@
+@using LibMatrix.Helpers
+@using LibMatrix.RoomTypes
+<tr>
+ <td>Room upgrade options</td>
+ <td>
+ @* <details> *@
+ @* <summary>Upgrading from @roomUpgrade.OldRoom.RoomId</summary> *@
+ <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.InviteMembers"></InputCheckbox>
+ <span>Invite members</span>
+ <br/>
+ <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.InvitePowerlevelUsers"></InputCheckbox>
+ <span>Invite users with powerlevels</span>
+ <br/>
+ <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.MigrateBans"></InputCheckbox>
+ <span>Copy bans (do not use with moderation bots!)</span>
+ <br/>
+ <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.MigrateEmptyStateEvents"></InputCheckbox>
+ <span>Include empty state events</span>
+ <br/>
+ <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.UpgradeUnstableValues"></InputCheckbox>
+ <span>Update unstable namespaced values to spec versions (experimental)</span>
+ <br/>
+ @if (roomUpgrade.Type == "support.feline.policy.lists.msc.v1") {
+ <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.Msc4321PolicyListUpgradeOptions.Enable"></InputCheckbox>
+ <span>Enable MSC4321 support</span>
+ <br/>
+ @if (roomUpgrade.UpgradeOptions.Msc4321PolicyListUpgradeOptions.Enable) {
+ <RoomCreateMsc4321UpgradeOptions roomUpgrade="@roomUpgrade" PageStateHasChanged="@PageStateHasChanged"/>
+ }
+ }
+ <LinkButton OnClickAsync="@(async () => {
+ await roomUpgrade.ImportAsync(OldRoom);
+ PageStateHasChanged();
+ })">Apply
+ </LinkButton>
+ @* </details> *@
+ </td>
+</tr>
+
+@code {
+
+ [Parameter]
+ public required GenericRoom OldRoom { get; set; }
+
+ [Parameter]
+ public required RoomUpgradeBuilder roomUpgrade { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor
index 01ab1c4..fc9c9bf 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,38 @@
// List<Task<RoomIdResponse>> tasks = Rooms.Select(room => room.JoinAsync(ServersInSpace.ToArray())).ToList();
// await Task.WhenAll(tasks);
foreach (var room in Rooms) {
- await room.JoinAsync(ServersInSpace.ToArray());
+ 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.Take(10).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;
+ await Task.Delay(1000);
+ }
+ }
+ 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..2af819b 100644
--- a/MatrixUtils.Web/Pages/Rooms/Timeline.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
@@ -2,7 +2,8 @@
@using MatrixUtils.Web.Shared.TimelineComponents
@using LibMatrix
@using LibMatrix.EventTypes.Spec
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Responses
<h3>RoomManagerTimeline</h3>
<hr/>
<p>Loaded @Events.Count events...</p>
@@ -27,7 +28,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..3da93f2 100644
--- a/MatrixUtils.Web/Pages/ServerInfo.razor
+++ b/MatrixUtils.Web/Pages/ServerInfo.razor
@@ -1,6 +1,7 @@
@page "/ServerInfo/{Homeserver}"
@using LibMatrix.Responses
@using ArcaneLibs.Extensions
+@using LibMatrix.Responses.Federation
<h3>Server info for @Homeserver</h3>
<hr/>
@if (ServerVersionResponse is not null) {
@@ -78,7 +79,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..7740596
--- /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}"))
+ // .ToAsyncResultEnumerable();
+ 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)}"))
+ .ToAsyncResultEnumerable();
+ 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/JoinRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor
new file mode 100644
index 0000000..cb56a40
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor
@@ -0,0 +1,70 @@
+@page "/Tools/Debug/JoinRoom"
+@using System.Collections.ObjectModel
+<h3>Join room</h3>
+<hr/>
+<span>Room ID: </span>
+<InputText @bind-Value="@RoomId"></InputText>
+<br/>
+<span>Via server(s), comma separated: </span>
+<InputText @bind-Value="@Servers"></InputText>
+<br/>
+<span>Unblock room (Synapse): </span>
+<InputCheckbox @bind-Value="@Unblock"></InputCheckbox>
+<br/>
+<LinkButton OnClickAsync="@Join">Join</LinkButton>
+<br/><br/>
+@foreach (var line in Log) {
+ <pre>@line</pre>
+ <br/>
+}
+
+@code {
+ AuthenticatedHomeserverGeneric? hs { get; set; }
+ ObservableCollection<string> Log { get; set; } = new ObservableCollection<string>();
+
+ [Parameter, SupplyParameterFromQuery(Name = "roomId")]
+ public string? RoomId { get; set; }
+
+ [Parameter, SupplyParameterFromQuery(Name = "via")]
+ public string? Servers { get; set; }
+
+ [Parameter, SupplyParameterFromQuery(Name = "unblock")]
+ public bool Unblock { get; set; } = false;
+
+ protected override async Task OnInitializedAsync() {
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is null) return;
+ Log.CollectionChanged += (sender, args) => StateHasChanged();
+
+ StateHasChanged();
+ Console.WriteLine("Rerendered!");
+ await base.OnInitializedAsync();
+ }
+
+ private async Task Join() {
+ if (string.IsNullOrWhiteSpace(RoomId)) return;
+ var room = hs.GetRoom(RoomId);
+ Log.Add("Got room object...");
+
+ if (Unblock && hs is AuthenticatedHomeserverSynapse synapse) {
+ try {
+ await synapse.Admin.BlockRoom(RoomId, false);
+ Log.Add($"Synapse: unblocked room");
+ }
+ catch (Exception e) {
+ Log.Add($"Synapse: failed to unblock room: {e}");
+ }
+ }
+
+ try {
+ await room.JoinAsync(Servers?.Split(','), checkIfAlreadyMember: false);
+ Log.Add("Joined room!");
+ }
+ catch (Exception e) {
+ Log.Add(e.ToString());
+ }
+
+ Log.Add("Done!");
+ }
+
+}
\ 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..c40fa0b 100644
--- a/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
@@ -1,11 +1,11 @@
-@page "/Tools/LeaveRoom"
+@page "/Tools/Debug/LeaveRoom"
@using System.Collections.ObjectModel
<h3>Leave room</h3>
<hr/>
<span>Room ID: </span>
<InputText @bind-Value="@RoomId"></InputText>
<br/>
-<LinkButton OnClick="@Leave">Leave</LinkButton>
+<LinkButton OnClickAsync="@Leave">Leave</LinkButton>
<br/><br/>
@foreach (var line in Log) {
<p>@line</p>
@@ -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..067036e 100644
--- a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
@@ -17,7 +17,7 @@
</details>
<br/>
-<LinkButton OnClick="Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="Execute">Execute</LinkButton>
<br/>
@foreach (var line in Enumerable.Reverse(log)) {
<p>@line</p>
@@ -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,13 +48,13 @@
}
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);
var members = await oldRoom.GetMembersListAsync();
- var tasks = members.Select(x => ExecuteInvite(hs, newRoom, x.StateKey)).ToAsyncEnumerable();
- // var tasks = hss.Select(ExecuteInvite).ToAsyncEnumerable();
+ var tasks = members.Select(x => ExecuteInvite(hs, newRoom, x.StateKey)).ToAsyncResultEnumerable();
+ // var tasks = hss.Select(ExecuteInvite).ToAsyncResultEnumerable();
await foreach (var a in tasks) {
if (!string.IsNullOrWhiteSpace(a)) {
log.Add(a);
@@ -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..a0abcd4 100644
--- a/MatrixUtils.Web/Pages/Tools/Index.razor
+++ b/MatrixUtils.Web/Pages/Tools/Index.razor
@@ -12,6 +12,7 @@
<a href="/Tools/User/MassRoomJoin">Join room across all session</a><br/>
<a href="/Tools/User/CopyPowerlevel">Copy highest powerlevel across all session</a><br/>
<a href="/Tools/User/ViewAccountData">View account data</a><br/>
+<a href="/Tools/User/StickerManager">Manage custom stickers and emojis</a><br/>
<h4 class="tool-category">Room tools</h4>
<hr/>
@@ -24,12 +25,13 @@
<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>
<hr/>
<a href="/Tools/Debug/SpaceDebug">Debug space relationships</a><br/>
+<a href="/Tools/Debug/JoinRoom">Join room by ID</a><br/>
<a href="/Tools/Debug/LeaveRoom">Leave room by ID</a><br/>
<a href="/Tools/Debug/MediaLocator">Locate lost media</a><br/>
<a href="/Tools/Debug/MigrateRoom">Migrate users from a split room to a new room</a><br/>
diff --git a/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
index ddd7b15..8ba160a 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().ToAsyncResultEnumerable();
+ // var fetchTasks = rooms.Select(async x => {
+ // await ss.WaitAsync();
+ // var res = await x.GetMembersByHomeserverAsync();
+ // ss.Release();
+ // return res;
+ // }).ToAsyncResultEnumerable();
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..bfd5fd3 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,74 @@ 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(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..dc5333b 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
@@ -4,14 +4,15 @@
@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/>
<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 OnClickAsync="@DoImportFromRoomId">Import from room (ID)</LinkButton>
<details>
<summary>Rooms to be searched (@rooms.Count)</summary>
@@ -21,7 +22,7 @@
}
</details>
<br/>
-<LinkButton OnClick="Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="Execute">Execute</LinkButton>
<br/>
<details>
@@ -44,9 +45,7 @@
@foreach (var (userId, events) in matches) {
<p>
<span>@userId.PadRight(col1Width)</span>
- @foreach (var @event in events) {
-
-}
+ @foreach (var @event in events) { }
</p>
}
</pre>
@@ -73,20 +72,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}");
}
}
@@ -97,7 +96,7 @@
rooms = new ObservableCollection<GenericRoom>(distinctRooms);
rooms.CollectionChanged += (sender, args) => StateHasChanged();
- var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync(false))).ToAsyncEnumerable();
+ var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync())).ToAsyncResultEnumerable();
await foreach (var (room, state) in stateTasks) {
roomMembers.Add(room, state);
@@ -106,7 +105,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..16a3853 100644
--- a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
+++ b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
@@ -1,24 +1,21 @@
@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/>
<br/>
<span>Room ID: </span>
<InputText @bind-Value="@roomId"></InputText>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<br/>
<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..5b0f510 100644
--- a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
+++ b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
@@ -1,19 +1,13 @@
@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/>
<br/>
<span>Users:</span>
<InputTextArea @bind-Value="@roomId"></InputTextArea>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<br/>
@@ -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..1ff97c8
--- /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 OnClickAsync="@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..9b0266c 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>
@@ -49,7 +49,7 @@
</div>
}
<br/>
-<LinkButton OnClick="@Apply">Apply</LinkButton>
+<LinkButton OnClickAsync="@Apply">Apply</LinkButton>
@code {
@@ -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..69a9048
--- /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 OnClickAsync="@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..5ad9de4
--- /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 OnClickAsync="Execute">Execute</LinkButton>
+<br/>
+<LinkButton OnClickAsync="RemoveKicks">Remove kicks</LinkButton>
+<LinkButton OnClickAsync="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;
+ }).ToAsyncResultEnumerable();
+ 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..ac68e3d 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
@@ -1,19 +1,21 @@
@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/>
<br/>
<span>Room ID: </span>
<InputText @bind-Value="@roomId"></InputText>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<br/>
<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..605890d 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -1,13 +1,14 @@
@page "/Tools/Moderation/MassCMEBan"
@using System.Collections.ObjectModel
@using LibMatrix.EventTypes.Spec.State.Policy
+@using LibMatrix.RoomTypes
<h3>User Trace</h3>
<hr/>
<br/>
<span>Users:</span>
<InputTextArea @bind-Value="@roomId"></InputTextArea>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<br/>
@@ -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..11c4a80 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>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
-<p><InputCheckbox @bind-Value="ChronologicalOrder"/> Chronological order</p>
+<InputText @bind-Value="@RoomId"></InputText>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
+<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 OnClickAsync="@(async () => {
+ ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false;
+ StateHasChanged();
+ })">Hide all
+ </LinkButton>
+ <LinkButton OnClickAsync="@(async () => {
+ ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true;
+ StateHasChanged();
+ })">Show all
+ </LinkButton>
+ <LinkButton OnClickAsync="@(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 OnClickAsync="@(async () => {
+ DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false;
+ StateHasChanged();
+ })">Un-disambiguate all
+ </LinkButton>
+ <LinkButton OnClickAsync="@(async () => {
+ DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true;
+ StateHasChanged();
+ })">Disambiguate all
+ </LinkButton>
+ <LinkButton OnClickAsync="@(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..ee77532 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
@@ -2,19 +2,19 @@
@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/>
<p>Set A: </p>
<InputText @bind-Value="@ImportSetASpaceId"></InputText>
-<LinkButton OnClick="@(() => AppendSet(ImportSetASpaceId, RoomsA))">Append Set A</LinkButton>
+<LinkButton OnClickAsync="@(() => AppendSet(ImportSetASpaceId, RoomsA))">Append Set A</LinkButton>
<p>Set B: </p>
<InputText @bind-Value="@ImportSetBSpaceId"></InputText>
-<LinkButton OnClick="@(() => AppendSet(ImportSetBSpaceId, RoomsB))">Append Set B</LinkButton>
+<LinkButton OnClickAsync="@(() => AppendSet(ImportSetBSpaceId, RoomsB))">Append Set B</LinkButton>
<br/>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<br/>
<details>
@@ -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();
@@ -144,7 +144,7 @@
public async Task GetMembers(List<GenericRoom> rooms, Dictionary<string, List<Match>> users) {
foreach (var room in rooms) {
Log.Add($"Getting members for {room.RoomId}");
- var members = await room.GetMembersListAsync(false);
+ var members = await room.GetMembersListAsync();
foreach (var member in members) {
if (member.RawContent?["membership"]?.ToString() == "ban") continue;
if (member.RawContent?["membership"]?.ToString() == "invite") continue;
@@ -158,7 +158,7 @@
}
public async Task AppendSet(string spaceId, List<GenericRoom> rooms) {
- var space = hs.GetRoom(spaceId).AsSpace;
+ var space = hs.GetRoom(spaceId).AsSpace();
Log.Add($"Found space {spaceId}");
var roomIdsEnum = space.GetChildrenAsync(true);
List<Task> tasks = new();
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
index 915f8dc..2261cb8 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 OnClickAsync="@DoImportFromRoomId">Import from room (ID)</LinkButton>
<details>
<summary>Rooms to be searched (@rooms.Count)</summary>
@@ -19,23 +21,26 @@
}
</details>
<br/>
-<LinkButton OnClick="Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="Execute">Execute</LinkButton>
<br/>
<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,17 +145,23 @@
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;
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var result in results) {
if (result is not null) {
yield return result;
@@ -191,4 +169,26 @@
}
}
+ 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/DropPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/Room/DropPowerlevel.razor
new file mode 100644
index 0000000..208cd19
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Room/DropPowerlevel.razor
@@ -0,0 +1,51 @@
+@page "/Tools/Room/DropPowerlevel"
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+<h3>DropPowerlevel</h3>
+<hr/>
+
+<span>User ID: </span><FancyTextBox @bind-Value="@UserId"/><br/>
+<span>Room ID: </span><FancyTextBox @bind-Value="@RoomId"/><br/>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
+
+<pre>@Result</pre>
+
+@code {
+ private AuthenticatedHomeserverGeneric? Homeserver { get; set; } = null!;
+
+ [Parameter, SupplyParameterFromQuery(Name = "RoomId")]
+ public string RoomId { get; set; } = "";
+
+ [Parameter, SupplyParameterFromQuery(Name = "UserId")]
+ public string UserId { get; set; } = "";
+
+ private string Result { get; set; } = "";
+
+ protected override async Task OnInitializedAsync() {
+ Homeserver = await sessionStore.GetCurrentHomeserver();
+ Result = "I am: " + Homeserver.WhoAmI.ToJson() + "\n";
+ StateHasChanged();
+ }
+
+ private async Task Execute() {
+ try {
+ if (Homeserver is not AuthenticatedHomeserverGeneric hs) {
+ Result = "Not authenticated";
+ return;
+ }
+
+ var room = hs.GetRoom(RoomId);
+
+ var powerlevels = await room.GetPowerLevelsAsync();
+ powerlevels.Users.Remove(UserId);
+ Result = (await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, powerlevels)).ToJson();
+ }
+ catch (Exception e) {
+ Result = e.Message;
+ }
+ finally {
+ StateHasChanged();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor b/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor
new file mode 100644
index 0000000..a47d7f5
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor
@@ -0,0 +1,204 @@
+@page "/Tools/Room/SpacePermissions"
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.RoomTypes
+@using MatrixUtils.Web.Pages.Rooms
+<h3>Space Permissions</h3>
+<hr/>
+<span>Space ID: </span>
+<FancyTextBox @bind-Value="@SpaceId"/>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
+<br/>
+<InputCheckbox @bind-Value="@AutoRecurseSpaces"/>
+<span> Auto-recurse into child spaces</span>
+<br/>
+
+@if (RoomPowerLevels.Count == 0) {
+ <p>No data loaded.</p>
+}
+else {
+ <span>Loaded @LoadedSpaceRooms.Count spaces.</span>
+ <br/>
+ @if (SpaceRooms.Count > 0) {
+ <h3>Load more spaces:</h3>
+ @foreach (var room in SpaceRooms) {
+ <LinkButton OnClickAsync="@(() => LoadSpaceAsync(room.Key))">@room.Value</LinkButton>
+ }
+ }
+
+ <h3>By event type:</h3>
+ <table class="table-striped table-hover table-bordered align-middle">
+ <thead>
+ <td>Room</td>
+ @foreach (var key in OrderedEventTypes) {
+ <td>@key.Key
+ <br/>
+ ~ @Math.Round(key.Value, 2)
+ </td>
+ }
+ </thead>
+ <tbody>
+ @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Events!.Values.Average())) {
+ <tr>
+ <td>@roomName</td>
+ @foreach (var eventType in OrderedEventTypes) {
+ if (!powerLevels.Events!.ContainsKey(eventType.Key)) {
+ <td style="background-color: #ff000044;">-</td>
+ continue;
+ }
+
+ <td>@(powerLevels.Events![eventType.Key])</td>
+ }
+ </tr>
+ }
+ </tbody>
+ </table>
+ <br/>
+ <h3>By user:</h3>
+ <table class="table-striped table-hover table-bordered align-middle">
+ <thead>
+ <td>Room</td>
+ @foreach (var key in OrderedUsers) {
+ <td>@key.Key
+ <br/>
+ ~ @Math.Round(key.Value, 2)
+ </td>
+ }
+ </thead>
+ <tbody>
+ @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Users!.Values.Average())) {
+ <tr>
+ <td>@roomName</td>
+ @foreach (var eventType in OrderedUsers) {
+ if (!powerLevels.Users!.ContainsKey(eventType.Key)) {
+ <td style="background-color: #ff000044;">-</td>
+ continue;
+ }
+
+ <td>@(powerLevels.Users![eventType.Key])</td>
+ }
+ </tr>
+ }
+ </tbody>
+ </table>
+}
+
+@code {
+
+ [Parameter, SupplyParameterFromQuery]
+ public string? SpaceId { get; set; }
+
+ [Parameter, SupplyParameterFromQuery]
+ public bool AutoRecurseSpaces { get; set; }
+
+ private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+ private List<AuthenticatedHomeserverGeneric> AllHomeservers { get; set; } = [];
+ private Dictionary<string, List<GenericRoom>> JoinedHomeserversByRoom { get; set; } = [];
+
+ private Dictionary<string, RoomPowerLevelEventContent> RoomPowerLevels { get; set; } = [];
+ private Dictionary<string, string> SpaceRooms { get; set; } = [];
+ private List<string> LoadedSpaceRooms { get; set; } = [];
+
+ private Dictionary<string, double> OrderedEventTypes { get; set; } = new();
+ private Dictionary<string, double> OrderedUsers { get; set; } = new();
+
+ protected override async Task OnInitializedAsync() {
+ if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not AuthenticatedHomeserverGeneric hs) return;
+ Homeserver = hs;
+ await foreach (var server in sessionStore.TryGetAllHomeservers()) {
+ AllHomeservers.Add(server);
+ var joinedRooms = await server.GetJoinedRooms();
+ foreach (var room in joinedRooms) {
+ if (!JoinedHomeserversByRoom.ContainsKey(room.RoomId)) {
+ JoinedHomeserversByRoom[room.RoomId] = [];
+ }
+
+ JoinedHomeserversByRoom[room.RoomId].Add(room);
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(SpaceId)) {
+ await Execute();
+ }
+ }
+
+ private async Task Execute() {
+ RoomPowerLevels = [];
+ SpaceRooms = [];
+ await LoadSpaceAsync(SpaceId);
+ }
+
+ private async Task<GenericRoom> GetJoinedRoomAsync(string roomId) {
+ var room = Homeserver.GetRoom(roomId);
+ if (await room.IsJoinedAsync()) return room;
+
+ if (JoinedHomeserversByRoom.TryGetValue(roomId, out var rooms)) {
+ foreach (var r in rooms) {
+ if (await r.IsJoinedAsync()) return r;
+ }
+ }
+
+ foreach (var hs in AllHomeservers) {
+ if (hs == Homeserver) continue;
+ room = hs.GetRoom(roomId);
+ if (await room.IsJoinedAsync()) return room;
+ }
+
+ Console.WriteLine($"Not joined to room {roomId} on any known homeserver.");
+ return room; // not null, in case we can preview the room
+ }
+
+ private async Task LoadSpaceAsync(string spaceId) {
+ LoadedSpaceRooms.Add(spaceId);
+ SpaceRooms.Remove(spaceId);
+
+ var space = (await GetJoinedRoomAsync(spaceId)).AsSpace();
+ RoomPowerLevels[await space.GetNameOrFallbackAsync()] = AddFakeEvents(await space.GetPowerLevelsAsync());
+ var children = space.GetChildrenAsync();
+ await foreach (var childRoom in children) {
+ var child = await GetJoinedRoomAsync(childRoom.RoomId);
+ try {
+ var powerlevels = await child.GetPowerLevelsAsync();
+ RoomPowerLevels[await child.GetNameOrFallbackAsync()] = AddFakeEvents(powerlevels!);
+ if (await child.GetRoomType() == SpaceRoom.TypeName) {
+ if (AutoRecurseSpaces)
+ await LoadSpaceAsync(child.RoomId);
+ else
+ SpaceRooms.Add(child.RoomId, await child.GetNameOrFallbackAsync());
+ }
+
+ OrderedEventTypes = RoomPowerLevels
+ .SelectMany(x => x.Value.Events!)
+ .GroupBy(x => x.Key)
+ .ToDictionary(x => x.Key, x => x.Average(y => y.Value))
+ .OrderByDescending(x => x.Value)
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ OrderedUsers = RoomPowerLevels
+ .SelectMany(x => x.Value.Users!)
+ .GroupBy(x => x.Key)
+ .ToDictionary(x => x.Key, x => x.Average(y => y.Value))
+ .OrderByDescending(x => x.Value)
+ .ToDictionary(x => x.Key, x => x.Value);
+ StateHasChanged();
+ }
+ catch (Exception ex) {
+ Console.WriteLine($"Failed to get power levels for room {child.RoomId}: {ex}");
+ }
+ }
+ }
+
+ private RoomPowerLevelEventContent AddFakeEvents(RoomPowerLevelEventContent powerlevels) {
+ powerlevels.Events ??= [];
+ powerlevels.Events["[user_default]"] = powerlevels.UsersDefault ?? 0;
+ powerlevels.Events["[event_default]"] = powerlevels.EventsDefault ?? 0;
+ powerlevels.Events["[state_default]"] = powerlevels.StateDefault ?? 100;
+ powerlevels.Events["[ban]"] = powerlevels.Ban ?? 100;
+ powerlevels.Events["[invite]"] = powerlevels.Invite ?? 100;
+ powerlevels.Events["[kick]"] = powerlevels.Kick ?? 100;
+ powerlevels.Events["[ping_room]"] = powerlevels.NotificationsPl?.Room ?? 100;
+ powerlevels.Events["[redact]"] = powerlevels.Redact ?? 100;
+ return powerlevels;
+ }
+
+}
\ 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..d6ae945 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/>
@@ -10,7 +10,7 @@
<p><InputCheckbox @bind-Value="@ChangeKnocking"/> Change knock access: <InputCheckbox @bind-Value="@Knocking"/></p>
<br/>
-<LinkButton OnClick="Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="Execute">Execute</LinkButton>
<br/>
<br/>
@@ -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();
@@ -40,7 +40,7 @@
}
private async Task Execute() {
- var space = hs.GetRoom(RoomId).AsSpace;
+ var space = hs.GetRoom(RoomId).AsSpace();
await foreach (var room in space.GetChildrenAsync()) {
log.Add($"Got room {room.RoomId}");
if (ChangeGuestAccess) {
diff --git a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
index 667b518..acc86a2 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/>
@@ -12,7 +12,7 @@
}
<br/>
-<LinkButton OnClick="Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="Execute">Execute</LinkButton>
<br/>
@foreach (var line in Enumerable.Reverse(log)) {
<p>@line</p>
@@ -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)).ToAsyncResultEnumerable();
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;
@@ -62,12 +62,11 @@
log.Add("I am same PL in " + room.RoomId);
continue;
}
-
+
pls.SetUserPowerLevel(ahs.WhoAmI.UserId, pls.GetUserPowerLevel(hs.WhoAmI.UserId));
await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, pls);
log.Add($"Updated powerlevel of {room.RoomId} to {pls.GetUserPowerLevel(ahs.WhoAmI.UserId)}");
}
-
}
catch (MatrixException e) {
return $"Failed to update PLs in {room.RoomId}: {e.Message}";
@@ -75,6 +74,7 @@
catch (Exception e) {
return $"Failed to update PLs in {room.RoomId}: {e.Message}";
}
+
StateHasChanged();
return "";
}
diff --git a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
index a2ad388..ee17f1d 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>
@@ -13,7 +13,7 @@
}
<br/>
-<LinkButton OnClick="Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="Execute">Execute</LinkButton>
<br/>
@foreach (var line in Enumerable.Reverse(log)) {
<p>@line</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();
@@ -42,23 +42,24 @@
}
private async Task Execute() {
- // foreach (var hs in hss) {
- // var rooms = await hs.GetJoinedRooms();
- var tasks = hss.Select(ExecuteInvite).ToAsyncEnumerable();
+ // foreach (var hs in hss) {
+ // var rooms = await hs.GetJoinedRooms();
+ var tasks = hss.Select(ExecuteInvite).ToAsyncResultEnumerable();
await foreach (var a in tasks) {
if (!string.IsNullOrWhiteSpace(a)) {
log.Add(a);
StateHasChanged();
}
}
- tasks = hss.Select(ExecuteJoin).ToAsyncEnumerable();
+
+ tasks = hss.Select(ExecuteJoin).ToAsyncResultEnumerable();
await foreach (var a in tasks) {
if (!string.IsNullOrWhiteSpace(a)) {
log.Add(a);
StateHasChanged();
}
}
- // }
+ // }
}
private async Task<string> ExecuteInvite(AuthenticatedHomeserverGeneric hs) {
@@ -69,6 +70,7 @@
if (joinRule.JoinRule == RoomJoinRulesEventContent.JoinRules.Public) return "Room is public, no invite needed";
}
catch { }
+
var pls = await room.GetPowerLevelsAsync();
if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) < pls.Invite) return "I do not have permission to send invite in " + room.RoomId;
await room.InviteUsersAsync(hss.Select(x => x.WhoAmI.UserId).ToList());
@@ -80,6 +82,7 @@
catch (Exception e) {
return $"Failed to invite in {room.RoomId}: {e.Message}";
}
+
StateHasChanged();
return "";
}
@@ -92,6 +95,7 @@
if (mse?.Membership == "join") return $"User {hs.WhoAmI.UserId} already in room";
}
catch { }
+
await room.JoinAsync();
}
catch (MatrixException e) {
@@ -100,6 +104,7 @@
catch (Exception e) {
return $"Failed to join {hs.WhoAmI.UserId} to {room.RoomId}: {e.Message}";
}
+
StateHasChanged();
return "";
}
diff --git a/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor
new file mode 100644
index 0000000..0e838c7
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor
@@ -0,0 +1,80 @@
+@page "/Tools/User/StickerManager"
+@using System.Diagnostics
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Common
+@using LibMatrix.EventTypes.Spec
+@inject ILogger<StickerManager> Logger
+<h3>Sticker/emoji manager</h3>
+
+@if (TotalStepsProgress is not null) {
+ <SimpleProgressIndicator ObservableProgress="@TotalStepsProgress"/>
+ <br/>
+}
+@if (_observableProgressState is not null) {
+ <SimpleProgressIndicator ObservableProgress="@_observableProgressState"/>
+ <br/>
+}
+
+@code {
+
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!;
+ private Msc2545EmoteRoomsAccountDataEventContent? EnabledEmoteRooms { get; set; }
+ private Dictionary<string, StickerRoom> StickerRooms { get; set; } = [];
+
+ private SimpleProgressIndicator.ObservableProgressState? _observableProgressState;
+
+ private SimpleProgressIndicator.ObservableProgressState? TotalStepsProgress { get; set; } = new() {
+ Label = "Authenticating with Matrix...",
+ Max = 2,
+ Value = 0
+ };
+
+ protected override async Task OnInitializedAsync() {
+ if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not { } hs)
+ return;
+ Homeserver = hs;
+ TotalStepsProgress?.Next("Fetching enabled emote packs...");
+ _ = hs.GetAccountDataOrNullAsync<Msc2545EmoteRoomsAccountDataEventContent>(Msc2545EmoteRoomsAccountDataEventContent.EventId)
+ .ContinueWith(r => {
+ EnabledEmoteRooms = r.Result;
+ StateHasChanged();
+ });
+
+ TotalStepsProgress?.Next("Getting joined rooms...");
+ _observableProgressState = new() {
+ Label = "Loading rooms...",
+ Max = 1,
+ Value = 0
+ };
+ var rooms = await hs.GetJoinedRooms();
+ _observableProgressState.Max.Value = rooms.Count;
+ StateHasChanged();
+
+ var ss = new SemaphoreSlim(32, 32);
+ var ss1 = new SemaphoreSlim(1, 1);
+ var roomScanTasks = rooms.Select(async room => {
+ // await Task.Delay(Random.Shared.Next(100, 1000 + (rooms.Count * 100)));
+ // await ss.WaitAsync();
+ var state = await room.GetFullStateAsListAsync();
+ StickerRoom sr = new();
+ foreach (var evt in state) {
+ if (evt.Type == RoomEmotesEventContent.EventId) { }
+ }
+
+ // ss.Release();
+ // await ss1.WaitAsync();
+ Console.WriteLine("Got state for room " + room.RoomId);
+ // _observableProgressState.Next($"Got state for room {room.RoomId}");
+ // await Task.Delay(1);
+ // ss1.Release();
+ return room.RoomId;
+ })
+ .ToList();
+ await foreach (var roomScanResult in roomScanTasks.ToAsyncResultEnumerable()) {
+ _observableProgressState.Label.Value = roomScanResult;
+ }
+ }
+
+ private class StickerRoom { }
+
+}
\ No newline at end of file
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..2b7b6cf 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,13 +10,17 @@
<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>
- <InputFile OnChange="@AvatarChanged"></InputFile><br/>
- <LinkButton OnClick="@(() => UpdateProfile())">Update profile</LinkButton>
- <LinkButton OnClick="@(() => UpdateProfile(true))">Update profile (restore room overrides)</LinkButton>
+ <span>Display name: </span>
+ <FancyTextBox @bind-Value="@NewProfile.DisplayName"></FancyTextBox>
+ <br/>
+ <span>Avatar URL: </span>
+ <FancyTextBox @bind-Value="@NewProfile.AvatarUrl"></FancyTextBox>
+ <InputFile OnChange="@AvatarChanged"></InputFile>
+ <br/>
+ <LinkButton OnClickAsync="@(() => UpdateProfile())">Update profile</LinkButton>
+ <LinkButton OnClickAsync="@(() => UpdateProfile(true))">Update profile (restore room overrides)</LinkButton>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(Status)) {
@@ -28,24 +30,33 @@
<br/>
@* <details> *@
- <h4>Room profiles<hr></h4>
+ <h4>Room profiles
+ <hr>
+ </h4>
@foreach (var room in Rooms) {
<details class="details-compact">
<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>
- <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, room.Room.RoomId))"></InputFile><br/>
- <LinkButton OnClick="@(() => UpdateRoomProfile(room.Room.RoomId))">Update profile</LinkButton>
+ <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>
+ <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, room.Room.RoomId))"></InputFile>
+ <br/>
+ <LinkButton OnClickAsync="@(() => UpdateRoomProfile(room.Room.RoomId))">Update profile</LinkButton>
</div>
<br/>
@if (!string.IsNullOrWhiteSpace(Status)) {
@@ -58,29 +69,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 +92,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 +100,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);
+ // }).ToAsyncResultEnumerable();
- 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..e48782f 100644
--- a/MatrixUtils.Web/Program.cs
+++ b/MatrixUtils.Web/Program.cs
@@ -1,13 +1,17 @@
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
+using ArcaneLibs.Blazor.Components.Services;
using Blazored.LocalStorage;
using Blazored.SessionStorage;
+using LibMatrix.Extensions;
using LibMatrix.Services;
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 +20,17 @@ 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(out var jsRuntime);
+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 = -1;
+ // 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"));
@@ -33,8 +48,7 @@ catch (Exception e) {
Console.WriteLine("Could not load appsettings: " + e);
}
-builder.Logging.AddConfiguration(
- builder.Configuration.GetSection("Logging"));
+builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
builder.Services.AddBlazoredLocalStorage(config => {
config.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
@@ -63,6 +77,13 @@ builder.Services.AddScoped<TieredStorageService>(x =>
)
);
+MatrixHttpClient.LogRequests = false;
+
builder.Services.AddRoryLibMatrixServices();
-builder.Services.AddScoped<RMUStorageWrapper>();
-await builder.Build().RunAsync();
\ No newline at end of file
+builder.Services.AddScoped<RmuSessionStore>();
+builder.Services.AddSingleton<BlazorSaveFileService>();
+builder.Services.AddSingleton<JsConsoleService>();
+
+// await builder.Build().RunAsync();
+var host = App.Host = builder.Build();
+await host.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/FilterComponents/BooleanFilterComponent.razor b/MatrixUtils.Web/Shared/FilterComponents/BooleanFilterComponent.razor
new file mode 100644
index 0000000..0730701
--- /dev/null
+++ b/MatrixUtils.Web/Shared/FilterComponents/BooleanFilterComponent.razor
@@ -0,0 +1,17 @@
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters
+<span>
+ <InputCheckbox @bind-Value="@Filter.Enabled"/> @Label:
+ @if (Filter.Enabled) {
+ <InputCheckbox @bind-Value="@Filter.Value"/>
+ }
+</span>
+
+@code {
+
+ [Parameter]
+ public required BoolFilter Filter { get; set; }
+
+ [Parameter]
+ public required string Label { get; set; }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/FilterComponents/StringFilterComponent.razor b/MatrixUtils.Web/Shared/FilterComponents/StringFilterComponent.razor
new file mode 100644
index 0000000..c5a6e15
--- /dev/null
+++ b/MatrixUtils.Web/Shared/FilterComponents/StringFilterComponent.razor
@@ -0,0 +1,31 @@
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters
+<span style="vertical-align: top;">
+ <InputCheckbox @bind-Value="@Filter.Enabled"/> @Label:
+</span>
+@if (Filter.Enabled) {
+ <div style="display: inline-block;">
+ <InputCheckbox @bind-Value="@Filter.CheckValueContains"/>
+ Contains
+ <FancyTextBox @bind-Value="@Filter.ValueContains"></FancyTextBox>
+ <br/>
+ <InputCheckbox @bind-Value="@Filter.CheckValueEquals"/>
+ Equals
+ <FancyTextBox @bind-Value="@Filter.ValueEquals"></FancyTextBox>
+ <LinkButton OnClick="@SetEqualsNull" InlineText="true"> [Set null]</LinkButton>
+ </div>
+}
+
+@code {
+
+ [Parameter]
+ public required StringFilter Filter { get; set; }
+
+ [Parameter]
+ public required string Label { get; set; }
+
+ private void SetEqualsNull() {
+ Filter.ValueEquals = null;
+ StateHasChanged();
+ }
+
+}
\ No newline at end of file
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/InputLocalPart.razor b/MatrixUtils.Web/Shared/InputLocalPart.razor
new file mode 100644
index 0000000..8f34377
--- /dev/null
+++ b/MatrixUtils.Web/Shared/InputLocalPart.razor
@@ -0,0 +1,50 @@
+<div style="display: inline-flex;">
+ @if (!string.IsNullOrWhiteSpace(Label)) {
+ <label>@Label</label>
+ }
+ <span>@Sigil</span>
+ <FancyTextBox @bind-Value="@LocalPart"></FancyTextBox>
+ <span>:</span>
+ @if (ServerNameChanged is not null) {
+ <FancyTextBox @bind-Value="@ServerName"></FancyTextBox>
+ }
+ else {
+ <span>@ServerName</span>
+ }
+</div>
+
+@code {
+
+ [Parameter]
+ public string? Label { get; set; }
+
+ [Parameter]
+ public required string Sigil { get; set; }
+
+ [Parameter]
+ public string? LocalPart {
+ get;
+ set {
+ if (field == value) return;
+ field = value;
+ LocalPartChanged.InvokeAsync(value);
+ }
+ }
+
+ [Parameter]
+ public EventCallback<string> LocalPartChanged { get; set; }
+
+ [Parameter]
+ public string? ServerName {
+ get;
+ set {
+ if (field == value) return;
+ field = value;
+ ServerNameChanged?.InvokeAsync(value);
+ }
+ }
+
+ [Parameter]
+ public EventCallback<string>? ServerNameChanged { get; set; }
+
+}
\ No newline at end of file
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..bb4b672
--- /dev/null
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
@@ -0,0 +1,221 @@
+@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/>
+ <FancyTextBox Multiline="true" @bind-Value="@Entities"></FancyTextBox>
+ <br/>
+
+
+ @* <details> *@
+ @* <summary>JSON data</summary> *@
+ @* <pre> *@
+ @* $1$ @PolicyEvent.ToJson(true, true) #1# *@
+ @* </pre> *@
+ @* </details> *@
+ @if (!VerifyIntent) {
+ <LinkButton OnClickAsync="@(() => {
+ OnClose.Invoke();
+ return Task.CompletedTask;
+ })"> Cancel
+ </LinkButton>
+ <LinkButton OnClickAsync="@(() => {
+ _ = Save();
+ return Task.CompletedTask;
+ })"> Save
+ </LinkButton>
+ @if (!string.IsNullOrWhiteSpace(Response)) {
+ <pre style="color: red;">@Response</pre>
+ }
+ }
+ else {
+ <b class="blink">WARNING!!!</b>
+ <br/>
+
+ @if (!string.IsNullOrWhiteSpace(Response)) {
+ <pre style="color: red;">@Response</pre>
+ }
+
+ <span>Are you sure you want to do this?</span>
+ <LinkButton Color="#00FF00" OnClick="@(() => {
+ VerifyIntent = false;
+ Response = null;
+ StateHasChanged();
+ })">No
+ </LinkButton>
+ <LinkButton Color="#FF0000" OnClick="@(() => { _ = Save(force: true); })">Yes</LinkButton>
+ }
+
+</ModalWindow>
+
+@code {
+
+ [Parameter]
+ public required Action OnClose { get; set; }
+
+ [Parameter]
+ public required Action OnSaved { get; set; }
+
+ [Parameter]
+ public required GenericRoom Room { get; set; }
+
+ private string Recommendation { get; set; } = "m.ban";
+ private string Reason { get; set; } = "spam";
+
+ private string Entities { get; set; } = "";
+
+ private string? Response {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private bool VerifyIntent { 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 static FrozenSet<string> AllKnownPolicyTypes = KnownPolicyTypes
+ .SelectMany(x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName))
+ .ToFrozenSet();
+
+ private string? MappedType { get; set; }
+
+ private async Task Save(bool force = false) {
+ if (string.IsNullOrWhiteSpace(MappedType)) {
+ Response = "No type selected";
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(Entities)) {
+ Response = "No users selected";
+ return;
+ }
+
+ Console.WriteLine("Saving ---");
+
+ var entities = Entities.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(x => x.Trim())
+ .Distinct()
+ .ToList();
+
+ if (!force && !Validate(entities, PolicyTypes[MappedType])) {
+ List<string> distinctTypes = entities
+ .Select(GuessType)
+ .Where(x => x != null)
+ .Distinct()
+ .Select(x => x!.Name)
+ .ToList();
+
+ VerifyIntent = true;
+ Response = $"Invalid entities. Expected {PolicyTypes[MappedType].Name}, got:\n - " +
+ string.Join("\n - ", distinctTypes);
+ return;
+ }
+
+ try {
+ await SaveAll(entities);
+ }
+ catch (Exception e) {
+ Response = $"Failed to save: {e}";
+ }
+ }
+
+ private bool Validate(List<string> entities, Type expectedType) {
+ return entities.All(x => GuessType(x) == expectedType);
+ }
+
+ private Type? GuessType(string entity) {
+ var sigil = entity[0];
+ return TypesBySigil.GetValueOrDefault(sigil.ToString(), typeof(ServerPolicyRuleEventContent));
+ }
+
+ private Dictionary<string, Type> TypesBySigil = new() {
+ { "@", typeof(UserPolicyRuleEventContent) },
+ { "!", typeof(RoomPolicyRuleEventContent) },
+ { "#", typeof(RoomPolicyRuleEventContent) }
+ };
+
+ private async Task SaveAll(List<string> entities) {
+ await foreach (var evt in Room.GetFullStateAsync()) {
+ if (evt is null
+ || !AllKnownPolicyTypes.Contains(evt.Type)
+ || !evt.TypedContent!.GetType().IsAssignableTo(PolicyTypes[MappedType!])
+ ) continue;
+
+ if (evt.TypedContent is PolicyRuleEventContent content && content.Recommendation == Recommendation && content.Reason == Reason) {
+ if (content.Entity != null && entities.Contains(content.Entity))
+ entities.Remove(content.Entity);
+ }
+ }
+
+ // var tasks = entities.Select(x => ExecuteBan(Room, x)).ToList();
+ // await Task.WhenAll(tasks);
+
+ var events = entities.Select(entity => {
+ var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent ?? throw new InvalidOperationException("Failed to create event content");
+ content.Recommendation = Recommendation;
+ content.Reason = Reason;
+ content.Entity = entity;
+ return new StateEvent() {
+ Type = MappedType,
+ TypedContent = content,
+ StateKey = content.GetDraupnir2StateKey()
+ };
+ });
+
+ foreach(var chunk in events.Chunk(50))
+ await Room.BulkSendEventsAsync(chunk);
+
+ OnSaved.Invoke();
+ }
+
+ private async Task ExecuteBan(GenericRoom room, string entity) {
+ bool success = false;
+ while (!success) {
+ try {
+ var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent ?? throw new InvalidOperationException("Failed to create event content");
+ 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/MassPolicyEditorModal.razor.css b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css
new file mode 100644
index 0000000..49ab31b
--- /dev/null
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css
@@ -0,0 +1,15 @@
+.blink {
+ animation: blinker 2s linear infinite;
+}
+
+@keyframes blinker {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
index 1bd00d1..0205e16 100644
--- a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
@@ -6,7 +6,7 @@
@using System.Collections.Frozen
@using LibMatrix.EventTypes
<ModalWindow Title="@((string.IsNullOrWhiteSpace(PolicyEvent.EventId) ? "Creating new " : "Editing ") + (PolicyEvent.MappedType.GetFriendlyNameOrNull()?.ToLower() ?? "event"))"
- OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="300">
+ OnCloseClickedAsync="@InvokeOnClose" X="60" Y="60" MinWidth="300">
@if (string.IsNullOrWhiteSpace(PolicyEvent.EventId)) {
<span>Policy type:</span>
<select @bind="@MappedType">
@@ -35,39 +35,75 @@
</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>
- <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> *@
+ <details>
+ <summary>JSON data</summary>
+ <pre>
+ @PolicyEvent.ToJson(true, true)
+ </pre>
+ </details>
+ <LinkButton OnClickAsync="@InvokeOnClose">Cancel</LinkButton>
+ <LinkButton OnClickAsync="@InvokeOnSave">Save</LinkButton>
}
else {
<p>Policy data is null</p>
@@ -89,10 +125,32 @@
}
[Parameter]
- public required Action OnClose { get; set; }
+ public Action? OnClose { get; set; }
[Parameter]
- public required Action<StateEventResponse> OnSave { get; set; }
+ public Func<Task>? OnCloseAsync { get; set; }
+
+ private async Task InvokeOnClose() {
+ if (OnClose is not null)
+ OnClose.Invoke();
+
+ if (OnCloseAsync is not null)
+ await OnCloseAsync.Invoke();
+ }
+
+ [Parameter]
+ public Action<StateEventResponse>? OnSave { get; set; }
+
+ [Parameter]
+ public Func<StateEventResponse, Task>? OnSaveAsync { get; set; }
+
+ private async Task InvokeOnSave() {
+ if (OnSave is not null)
+ OnSave.Invoke(PolicyEvent);
+
+ if (OnSaveAsync is not null)
+ await OnSaveAsync.Invoke(PolicyEvent);
+ }
public PolicyRuleEventContent? PolicyData { get; set; }
@@ -102,7 +160,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 +168,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..471f586 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 rooms = Space.Room.AsSpace().GetChildrenAsync();
+ 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..fd2fdec 100644
--- a/MatrixUtils.Web/Shared/UserListItem.razor
+++ b/MatrixUtils.Web/Shared/UserListItem.razor
@@ -1,7 +1,12 @@
@using LibMatrix.Responses
@using ArcaneLibs
<div style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-Content;">
- <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%;" src="@(string.IsNullOrWhiteSpace(User?.AvatarUrl) ? _identiconGenerator.GenerateAsDataUri(UserId) : User.AvatarUrl)"/>
+ @if (!string.IsNullOrWhiteSpace(User?.AvatarUrl)) {
+ <MxcAvatar Homeserver="@_homeserver" Size="32" Circular="true" MxcUri="@User.AvatarUrl"/>
+ }
+ else {
+ <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%;" src="@_identiconGenerator.GenerateAsDataUri(UserId)"/>
+ }
<span style="vertical-align: middle; margin-right: 8px; border-radius: 75px;">@User?.DisplayName</span>
<div style="display: inline-block;">
@@ -23,20 +28,28 @@
[Parameter]
public string UserId { get; set; }
- private AuthenticatedHomeserverGeneric _homeserver = null!;
+ [Parameter]
+ public AuthenticatedHomeserverGeneric _homeserver { get; set; }
- private SvgIdenticonGenerator _identiconGenerator = new();
+ private static 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) {
throw new ArgumentNullException(nameof(UserId));
}
- User = await _homeserver.GetProfileAsync(UserId);
+ try {
+ User = await _homeserver.GetProfileAsync(UserId);
+ }
+ catch (Exception) {
+ User = new() {
+ DisplayName = UserId
+ };
+ }
}
await base.OnInitializedAsync();
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/appsettings.Development.json b/MatrixUtils.Web/appsettings.Development.json
index 1ca99ed..1555d4e 100644
--- a/MatrixUtils.Web/appsettings.Development.json
+++ b/MatrixUtils.Web/appsettings.Development.json
@@ -3,7 +3,10 @@
"LogLevel": {
"Default": "Trace",
"System": "Information",
- "Microsoft": "Information"
+ "Microsoft": "Information",
+ "Microsoft.AspNetCore.StaticAssets": "Warning",
+ "Microsoft.AspNetCore.EndpointMiddleware": "Warning",
+ "ArcaneLibs.Blazor.Components.AuthorizedImage": "Information"
}
}
}
diff --git a/MatrixUtils.Web/appsettings.json b/MatrixUtils.Web/appsettings.json
index 29d3614..f33cc65 100644
--- a/MatrixUtils.Web/appsettings.json
+++ b/MatrixUtils.Web/appsettings.json
@@ -3,7 +3,8 @@
"LogLevel": {
"Default": "Trace", //debug
"System": "Information",
- "Microsoft": "Information"
+ "Microsoft": "Information",
+ "ArcaneLibs.Blazor.Components.AuthorizedImage": "Information"
}
}
}
diff --git a/MatrixUtils.Web/wwwroot/appsettings.json b/MatrixUtils.Web/wwwroot/appsettings.json
index 1ca99ed..826edbf 100644
--- a/MatrixUtils.Web/wwwroot/appsettings.json
+++ b/MatrixUtils.Web/wwwroot/appsettings.json
@@ -3,7 +3,8 @@
"LogLevel": {
"Default": "Trace",
"System": "Information",
- "Microsoft": "Information"
+ "Microsoft": "Information",
+ "ArcaneLibs.Blazor.Components.AuthorizedImage": "Information"
}
}
}
diff --git a/MatrixUtils.Web/wwwroot/css/app.css b/MatrixUtils.Web/wwwroot/css/app.css
index 3fac9ca..4511b3a 100644
--- a/MatrixUtils.Web/wwwroot/css/app.css
+++ b/MatrixUtils.Web/wwwroot/css/app.css
@@ -1,6 +1,11 @@
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
@import url('jetbrains-mono/jetbrains-mono.css');
+:root {
+ /*--bs-table-hover-bg: rgba(0, 0, 0, 0.75);*/
+ --bs-table-hover-bg: #FF00FF;
+}
+
.avatar48 {
width: 48px;
height: 48px;
diff --git a/MatrixUtils.Web/wwwroot/index.html b/MatrixUtils.Web/wwwroot/index.html
index 5182193..0a80cff 100644
--- a/MatrixUtils.Web/wwwroot/index.html
+++ b/MatrixUtils.Web/wwwroot/index.html
@@ -12,6 +12,7 @@
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png"/>
<link href="favicon.png" rel="icon" type="image/png"/>
<link href="MatrixUtils.Web.styles.css" rel="stylesheet"/>
+ <link rel="preload" id="webassembly"/>
</head>
<body>
@@ -29,16 +30,6 @@
<a class="dismiss">🗙</a>
</div>
<script>
- function BlazorFocusElement(element) {
- if (element == null) return;
- if (element instanceof HTMLElement) {
- console.log(element);
- element.focus();
- } else if (element.hasOwnProperty("__internalId")) {
- console.log("Element is not an HTMLElement", element);
- }
- }
-
function getWidth(element) {
console.log("getWidth", element);
if (element == null) return 0;
@@ -57,9 +48,25 @@
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>-->
+ <!-- <script>navigator.serviceWorker.register('service-worker.js');</script>-->
<script src="sw-registrator.js"></script>
</body>
diff --git a/MatrixUtils.Web/wwwroot/sw-registrator.js b/MatrixUtils.Web/wwwroot/sw-registrator.js
index 94b96b2..67aa5cb 100644
--- a/MatrixUtils.Web/wwwroot/sw-registrator.js
+++ b/MatrixUtils.Web/wwwroot/sw-registrator.js
@@ -8,7 +8,7 @@ window.updateAvailable = new Promise((resolve, reject) => {
return;
}
- navigator.serviceWorker.register('/service-worker.js')
+ navigator.serviceWorker.register('/service-worker.js', {updateViaCache: 'none'})
.then(registration => {
console.info(`Service worker registration successful (scope: ${registration.scope})`);
diff --git a/MatrixUtils.sln b/MatrixUtils.sln
new file mode 100644
index 0000000..032c49b
--- /dev/null
+++ b/MatrixUtils.sln
@@ -0,0 +1,489 @@
+
+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
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Federation", "LibMatrix\LibMatrix.Federation\LibMatrix.Federation.csproj", "{8F154875-96EE-4BE5-8456-F5EBB2516C1C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.FederationTest", "LibMatrix\Utilities\LibMatrix.FederationTest\LibMatrix.FederationTest.csproj", "{960CC2DF-BB1A-4164-A895-834F81B3A113}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.RoomUpgradeCLI", "MatrixUtils.RoomUpgradeCLI\MatrixUtils.RoomUpgradeCLI.csproj", "{F0F10F51-4883-4C70-80D2-24D3AA8C0096}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ 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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|x64.Build.0 = Debug|Any CPU
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|x86.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
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|x64.ActiveCfg = Release|Any CPU
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|x64.Build.0 = Release|Any CPU
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|x86.ActiveCfg = Release|Any CPU
+ {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|x64.Build.0 = Debug|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|x86.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
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|x64.ActiveCfg = Release|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|x64.Build.0 = Release|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|x86.ActiveCfg = Release|Any CPU
+ {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|x64.Build.0 = Debug|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|x86.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
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|x64.ActiveCfg = Release|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|x64.Build.0 = Release|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|x86.ActiveCfg = Release|Any CPU
+ {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|x64.Build.0 = Debug|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|x86.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
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|x64.ActiveCfg = Release|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|x64.Build.0 = Release|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|x86.ActiveCfg = Release|Any CPU
+ {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|x64.Build.0 = Debug|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|x86.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
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|x64.ActiveCfg = Release|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|x64.Build.0 = Release|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|x86.ActiveCfg = Release|Any CPU
+ {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|x64.Build.0 = Debug|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|x86.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
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|x64.ActiveCfg = Release|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|x64.Build.0 = Release|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|x86.ActiveCfg = Release|Any CPU
+ {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|x64.Build.0 = Debug|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|x86.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
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|x64.ActiveCfg = Release|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|x64.Build.0 = Release|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|x86.ActiveCfg = Release|Any CPU
+ {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|x64.Build.0 = Debug|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|x86.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
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|x64.ActiveCfg = Release|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|x64.Build.0 = Release|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|x86.ActiveCfg = Release|Any CPU
+ {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|x64.Build.0 = Debug|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|x86.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
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|x64.ActiveCfg = Release|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|x64.Build.0 = Release|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|x86.ActiveCfg = Release|Any CPU
+ {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|x64.Build.0 = Debug|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|x86.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
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|x64.ActiveCfg = Release|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|x64.Build.0 = Release|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|x86.ActiveCfg = Release|Any CPU
+ {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|x64.Build.0 = Debug|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|x86.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
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|x64.ActiveCfg = Release|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|x64.Build.0 = Release|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|x86.ActiveCfg = Release|Any CPU
+ {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Debug|x64.Build.0 = Debug|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Debug|x86.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
+ {D6315791-949B-4501-AA95-50516DE899C1}.Release|x64.ActiveCfg = Release|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Release|x64.Build.0 = Release|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Release|x86.ActiveCfg = Release|Any CPU
+ {D6315791-949B-4501-AA95-50516DE899C1}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|x64.Build.0 = Debug|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|x86.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
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|x64.ActiveCfg = Release|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|x64.Build.0 = Release|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|x86.ActiveCfg = Release|Any CPU
+ {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Debug|x64.Build.0 = Debug|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Debug|x86.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
+ {0336306C-285A-4810-9253-5C5F0373992E}.Release|x64.ActiveCfg = Release|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Release|x64.Build.0 = Release|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Release|x86.ActiveCfg = Release|Any CPU
+ {0336306C-285A-4810-9253-5C5F0373992E}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|x64.Build.0 = Debug|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|x86.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
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|x64.ActiveCfg = Release|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|x64.Build.0 = Release|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|x86.ActiveCfg = Release|Any CPU
+ {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|x64.Build.0 = Debug|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|x86.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
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|x64.ActiveCfg = Release|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|x64.Build.0 = Release|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|x86.ActiveCfg = Release|Any CPU
+ {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|x64.Build.0 = Debug|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|x86.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
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|x64.ActiveCfg = Release|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|x64.Build.0 = Release|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|x86.ActiveCfg = Release|Any CPU
+ {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|x64.Build.0 = Debug|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|x86.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
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|x64.ActiveCfg = Release|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|x64.Build.0 = Release|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|x86.ActiveCfg = Release|Any CPU
+ {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|x64.Build.0 = Debug|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|x86.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
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|x64.ActiveCfg = Release|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|x64.Build.0 = Release|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|x86.ActiveCfg = Release|Any CPU
+ {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|x64.Build.0 = Debug|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|x86.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
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|x64.ActiveCfg = Release|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|x64.Build.0 = Release|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|x86.ActiveCfg = Release|Any CPU
+ {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|x64.Build.0 = Debug|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|x86.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
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|x64.ActiveCfg = Release|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|x64.Build.0 = Release|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|x86.ActiveCfg = Release|Any CPU
+ {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|x64.Build.0 = Debug|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|x86.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
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|x64.ActiveCfg = Release|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|x64.Build.0 = Release|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|x86.ActiveCfg = Release|Any CPU
+ {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|x64.Build.0 = Debug|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|x86.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
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|x64.ActiveCfg = Release|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|x64.Build.0 = Release|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|x86.ActiveCfg = Release|Any CPU
+ {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|x64.Build.0 = Debug|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|x86.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
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|x64.ActiveCfg = Release|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|x64.Build.0 = Release|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|x86.ActiveCfg = Release|Any CPU
+ {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|x64.Build.0 = Debug|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|x86.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
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|x64.ActiveCfg = Release|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|x64.Build.0 = Release|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|x86.ActiveCfg = Release|Any CPU
+ {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|x64.Build.0 = Debug|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|x86.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
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|x64.ActiveCfg = Release|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|x64.Build.0 = Release|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|x86.ActiveCfg = Release|Any CPU
+ {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|x64.Build.0 = Debug|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|x86.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
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|x64.ActiveCfg = Release|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|x64.Build.0 = Release|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|x86.ActiveCfg = Release|Any CPU
+ {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|x64.Build.0 = Debug|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|x86.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
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|x64.ActiveCfg = Release|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|x64.Build.0 = Release|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|x86.ActiveCfg = Release|Any CPU
+ {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|x86.Build.0 = Release|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|x64.Build.0 = Debug|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|x86.Build.0 = Debug|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|x64.ActiveCfg = Release|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|x64.Build.0 = Release|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|x86.ActiveCfg = Release|Any CPU
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|x86.Build.0 = Release|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|x64.Build.0 = Debug|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|x86.Build.0 = Debug|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|Any CPU.Build.0 = Release|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|x64.ActiveCfg = Release|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|x64.Build.0 = Release|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|x86.ActiveCfg = Release|Any CPU
+ {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|x86.Build.0 = Release|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|x64.Build.0 = Debug|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|x86.Build.0 = Debug|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|x64.ActiveCfg = Release|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|x64.Build.0 = Release|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|x86.ActiveCfg = Release|Any CPU
+ {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ 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}
+ {8F154875-96EE-4BE5-8456-F5EBB2516C1C} = {933DC8A6-8B1F-46BF-9046-4B636AA46469}
+ {960CC2DF-BB1A-4164-A895-834F81B3A113} = {80828C75-9C5B-442F-86A4-8CE9D85E811C}
+ 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/
|