diff --git a/.idea/.idea.MatrixUtils/.idea/indexLayout.xml b/.idea/.idea.MatrixUtils/.idea/indexLayout.xml
index d166ec4..4520708 100644
--- a/.idea/.idea.MatrixUtils/.idea/indexLayout.xml
+++ b/.idea/.idea.MatrixUtils/.idea/indexLayout.xml
@@ -2,7 +2,6 @@
<project version="4">
<component name="UserContentModel">
<attachedFolders>
- <Path>LibMatrix/LibMatrix/Homeservers</Path>
<Path>MatrixRoomUtils.Bot/bot_data</Path>
<Path>MatrixRoomUtils.Desktop/bin/Debug/net7.0/mru-desktop</Path>
</attachedFolders>
diff --git a/.idea/.idea.MatrixUtils/.idea/vcs.xml b/.idea/.idea.MatrixUtils/.idea/vcs.xml
index 94a25f7..df05d42 100644
--- a/.idea/.idea.MatrixUtils/.idea/vcs.xml
+++ b/.idea/.idea.MatrixUtils/.idea/vcs.xml
@@ -2,5 +2,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/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj
index 5d584c9..06164d3 100644
--- a/Benchmarks/Benchmarks.csproj
+++ b/Benchmarks/Benchmarks.csproj
@@ -2,16 +2,16 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
-<!-- <PublishAot>true</PublishAot>-->
+ <!-- <PublishAot>true</PublishAot>-->
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
+ <PackageReference Include="BenchmarkDotNet" Version="0.15.8"/>
</ItemGroup>
</Project>
diff --git a/LibMatrix b/LibMatrix
-Subproject cacabe2b1a15bb7492e23d477ec653513e84d26
+Subproject 0640eba992f95cc45873330b76fadf123202d1c
diff --git a/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj b/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj
index 96f9fcb..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>net9.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 4b2a53c..1efbd6f 100644
--- a/MatrixUtils.Abstractions/RoomInfo.cs
+++ b/MatrixUtils.Abstractions/RoomInfo.cs
@@ -16,22 +16,22 @@ public class RoomInfo : NotifyPropertyChanged {
RegisterEventListener();
}
- public RoomInfo(GenericRoom room, List<StateEventResponse>? stateEvents) {
+ public RoomInfo(GenericRoom room, List<MatrixEventResponse>? stateEvents) {
Room = room;
// _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId);
if (stateEvents is { Count: > 0 }) StateEvents = new(stateEvents!);
RegisterEventListener();
ProcessNewItems(stateEvents!);
}
-
+
public readonly GenericRoom Room;
- public ObservableCollection<StateEventResponse?> StateEvents { get; private set; } = new();
- public ObservableCollection<StateEventResponse?> Timeline { get; private set; } = new();
+ public ObservableCollection<MatrixEventResponse?> StateEvents { get; private set; } = new();
+ public ObservableCollection<MatrixEventResponse?> Timeline { get; private set; } = new();
private static ConcurrentBag<AuthenticatedHomeserverGeneric> homeserversWithoutEventFormatSupport = new();
// private static SvgIdenticonGenerator identiconGenerator = new();
- public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") {
+ public async Task<MatrixEventResponse?> GetStateEvent(string type, string stateKey = "") {
if (homeserversWithoutEventFormatSupport.Contains(Room.Homeserver)) return await GetStateEventForged(type, stateKey);
var @event = StateEvents.FirstOrDefault(x => x?.Type == type && x.StateKey == stateKey);
if (@event is not null) return @event;
@@ -54,8 +54,8 @@ public class RoomInfo : NotifyPropertyChanged {
return @event;
}
- private async Task<StateEventResponse?> GetStateEventForged(string type, string stateKey = "") {
- var @event = new StateEventResponse {
+ private async Task<MatrixEventResponse?> GetStateEventForged(string type, string stateKey = "") {
+ var @event = new MatrixEventResponse {
RoomId = Room.RoomId,
Type = type,
StateKey = stateKey,
@@ -148,16 +148,16 @@ public class RoomInfo : NotifyPropertyChanged {
private void RegisterEventListener() {
StateEvents.CollectionChanged += (_, args) => {
if (args.NewItems is { Count: > 0 })
- ProcessNewItems(args.NewItems.OfType<StateEventResponse>());
+ ProcessNewItems(args.NewItems.OfType<MatrixEventResponse>());
};
}
- private void ProcessNewItems(IEnumerable<StateEventResponse?> newItems) {
- foreach (StateEventResponse? newState in newItems) {
+ private void ProcessNewItems(IEnumerable<MatrixEventResponse?> newItems) {
+ foreach (MatrixEventResponse? newState in newItems) {
if (newState is null) continue;
// TODO: Benchmark switch statement
-
- if(newState.StateKey != "") continue;
+
+ if (newState.StateKey != "") continue;
if (newState.Type == RoomNameEventContent.EventId && newState.TypedContent is RoomNameEventContent roomNameContent)
RoomName = roomNameContent.Name;
else if (newState is { Type: RoomAvatarEventContent.EventId, TypedContent: RoomAvatarEventContent roomAvatarContent })
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/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 a7ff4b9..70b64a8 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>net9.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.2.1" />
- <PackageReference Include="Avalonia.Desktop" Version="11.2.1" />
- <PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1" />
- <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.1" />
+ <PackageReference Include="Avalonia" Version="11.3.9" />
+ <PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
+ <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.9" />
+ <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.9" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
- <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.1" />
- <PackageReference Include="Sentry" Version="4.13.0" />
+ <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.1"/>
</ItemGroup>
-
-
<ItemGroup>
- <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.2.0" />
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
+ <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.3.0.6"/>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
</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 f8dd598..b02650d 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>net9.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" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="appsettings*.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
</Project>
diff --git a/MatrixUtils.DmSpaced/ModerationBot.cs b/MatrixUtils.DmSpaced/ModerationBot.cs
index 6e534fc..17a017b 100644
--- a/MatrixUtils.DmSpaced/ModerationBot.cs
+++ b/MatrixUtils.DmSpaced/ModerationBot.cs
@@ -57,9 +57,11 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
await _logRoom?.SendMessageEventAsync(MessageFormatter.FormatWarning($"Control room has no m.room.power_levels?"));
continue;
}
+
pls.SetUserPowerLevel(configurationAdmin, pls.GetUserPowerLevel(hs.UserId));
await _controlRoom.SendStateEventAsync(RoomPowerLevelEventContent.EventId, pls);
}
+
var syncHelper = new SyncHelper(hs);
List<string> admins = new();
@@ -85,7 +87,8 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
x.Type == "m.room.member" && x.StateKey == hs.UserId);
logger.LogInformation("Got invite to {RoomId} by {Sender} with reason: {Reason}", args.Key, inviteEvent!.Sender,
(inviteEvent.TypedContent as RoomMemberEventContent)!.Reason);
- await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Bot invited to {MessageFormatter.HtmlFormatMention(args.Key)} by {MessageFormatter.HtmlFormatMention(inviteEvent.Sender)}"));
+ await _logRoom.SendMessageEventAsync(
+ MessageFormatter.FormatSuccess($"Bot invited to {MessageFormatter.HtmlFormatMention(args.Key)} by {MessageFormatter.HtmlFormatMention(inviteEvent.Sender)}"));
if (admins.Contains(inviteEvent.Sender)) {
try {
await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joining {MessageFormatter.HtmlFormatMention(args.Key)}..."));
@@ -117,7 +120,8 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
var rules = await engine.GetMatchingPolicies(@event);
foreach (var matchedRule in rules) {
await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccessJson(
- $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.OriginalEvent.EventId, roomId: matchedRule.PolicyList.Room.RoomId, displayName: "rule")}", @matchedRule.OriginalEvent.RawContent));
+ $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.OriginalEvent.EventId, roomId: matchedRule.PolicyList.Room.RoomId, displayName: "rule")}",
+ @matchedRule.OriginalEvent.RawContent));
}
if (configuration.DemoMode) {
@@ -263,9 +267,10 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
await syncHelper.RunSyncLoopAsync();
}
- private async Task LogPolicyChange(StateEventResponse changeEvent) {
+ private async Task LogPolicyChange(MatrixEventResponse changeEvent) {
var room = hs.GetRoom(changeEvent.RoomId!);
- var message = MessageFormatter.FormatWarning($"Policy change detected in {MessageFormatter.HtmlFormatMessageLink(changeEvent.RoomId, changeEvent.EventId, [hs.ServerName], await room.GetNameOrFallbackAsync())}!");
+ var message = MessageFormatter.FormatWarning(
+ $"Policy change detected in {MessageFormatter.HtmlFormatMessageLink(changeEvent.RoomId, changeEvent.EventId, [hs.ServerName], await room.GetNameOrFallbackAsync())}!");
message = message.ConcatLine(new RoomMessageEventContent(body: $"Policy type: {changeEvent.Type} -> {changeEvent.MappedType.Name}") {
FormattedBody = $"Policy type: {changeEvent.Type} -> {changeEvent.MappedType.Name}"
});
@@ -281,11 +286,12 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
// else {
// message = message.ConcatLine(MessageFormatter.FormatSuccess("New rule added!"));
// }
- message = message.ConcatLine(MessageFormatter.FormatSuccessJson($"{(isUpdated ? "Updated" : isRemoved ? "Removed" : "New")} rule: {changeEvent.StateKey}", changeEvent.RawContent!));
+ message = message.ConcatLine(MessageFormatter.FormatSuccessJson($"{(isUpdated ? "Updated" : isRemoved ? "Removed" : "New")} rule: {changeEvent.StateKey}",
+ changeEvent.RawContent!));
if (isRemoved || isUpdated) {
message = message.ConcatLine(MessageFormatter.FormatSuccessJson("Old content: ", changeEvent.Unsigned.PrevContent!));
}
-
+
await _logRoom.SendMessageEventAsync(message);
}
@@ -294,5 +300,4 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
public async Task StopAsync(CancellationToken cancellationToken) {
logger.LogInformation("Shutting down bot!");
}
-
-}
+}
\ No newline at end of file
diff --git a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
index 646a3f3..dc88c75 100644
--- a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
+++ b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
@@ -32,7 +32,7 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI
else await ImportNativeDMsWithoutLayers();
}
- public async Task<List<StateEventResponse>> GetAllActiveLayersAsync() {
+ public async Task<List<MatrixEventResponse>> GetAllActiveLayersAsync() {
var state = await GetFullStateAsListAsync();
return state.Where(x => x.Type == DMSpaceChildLayer.EventId && x.RawContent.ContainsKey("space_id")).ToList();
}
@@ -58,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
@@ -117,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() {
@@ -140,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 e39440e..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>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LinkIncremental>true</LinkIncremental>
@@ -10,6 +10,6 @@
</PropertyGroup>
<ItemGroup>
- <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
+ <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj"/>
</ItemGroup>
</Project>
diff --git a/MatrixUtils.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..a095f2e
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Commands/ExecuteCommand.cs
@@ -0,0 +1,72 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using ArcaneLibs.Extensions;
+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}...");
+ if (rb is RoomUpgradeBuilder { CanUpgrade: false } rub && !(rub.UpgradeOptions.ForceUpgrade || rub.UpgradeOptions.NoopUpgrade)) {
+ Console.WriteLine("Warning: Import state has determined that you cannot upgrade this room.");
+ Console.WriteLine(rub.ToJson());
+ return;
+ }
+
+ 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..f6e5199
--- /dev/null
+++ b/MatrixUtils.RoomUpgradeCLI/Extensions/RoomBuilderExtensions.cs
@@ -0,0 +1,274 @@
+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 "--restrict-old-room":
+ if (rb is not RoomUpgradeBuilder upgradeBuilderRestrict) {
+ throw new InvalidOperationException("Restrict old room can only be used with room upgrades");
+ }
+
+ var oldRoom = hs.GetRoom(upgradeBuilderRestrict.OldRoomId);
+ var createEvt = await oldRoom.GetCreateEventAsync();
+ if (createEvt == null) {
+ throw new InvalidOperationException("Could not get create event for old room " + upgradeBuilderRestrict.OldRoomId);
+ }
+
+ if (!int.TryParse(createEvt.RoomVersion ?? "1", out int numericVersion)) {
+ Console.WriteLine("Warning: Could not parse old room version '" + createEvt.RoomVersion + "' as a number! Setting restricted join rule may not work.");
+ }
+ else if (numericVersion < 8) {
+ throw new InvalidOperationException(
+ "Cannot set restrict old room on rooms with version lower than 8!\nhttps://spec.matrix.org/v1.17/rooms/#feature-matrix"
+ );
+ }
+
+ upgradeBuilderRestrict.UpgradeOptions.RestrictOldRoom = GetBoolArg(args, ref i, true);
+ break;
+ 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..edca2f5
--- /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" />
+ </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..3a4c822
--- /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.AsEnumerable().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 24401ab..925266b 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>net9.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="9.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0" />
</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 a8cf817..7e8e1c3 100644
--- a/MatrixUtils.Web/App.razor
+++ b/MatrixUtils.Web/App.razor
@@ -1,4 +1,5 @@
-<Router AppAssembly="@typeof(App).Assembly">
+@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
+<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
@@ -10,3 +11,9 @@
</LayoutView>
</NotFound>
</Router>
+
+@code {
+
+ public static WebAssemblyHost Host { get; set; } = null!;
+
+}
diff --git a/MatrixUtils.Web/Classes/LocalStorageProviderService.cs b/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
index 0e99cd4..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<IEnumerable<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/RmuSessionStore.cs b/MatrixUtils.Web/Classes/RmuSessionStore.cs
index 7e5b155..1611b83 100644
--- a/MatrixUtils.Web/Classes/RmuSessionStore.cs
+++ b/MatrixUtils.Web/Classes/RmuSessionStore.cs
@@ -1,5 +1,3 @@
-using System.Text.Json;
-using System.Text.Json.Nodes;
using LibMatrix;
using LibMatrix.Homeservers;
using LibMatrix.Services;
@@ -28,6 +26,11 @@ public class RmuSessionStore(
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;
@@ -41,6 +44,11 @@ public class RmuSessionStore(
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);
}
@@ -54,25 +62,31 @@ public class RmuSessionStore(
SessionId = sessionId
};
+ await SaveStorage();
if (CurrentSession == null) await SetCurrentSession(sessionId);
- else await SaveStorage();
return sessionId;
}
public async Task RemoveSession(string sessionId) {
await LoadStorage();
- logger.LogTrace("Removing session {sessionId}.", sessionId);
- var tokens = await GetAllSessions();
- if (tokens == null) {
+ if (SessionCache.Count == 0) {
+ logger.LogWarning("No sessions found.");
return;
}
+ logger.LogTrace("Removing session {sessionId}.", sessionId);
+
if ((await GetCurrentSession())?.SessionId == sessionId)
- await SetCurrentSession(tokens.First(x => x.Key != sessionId).Key);
+ await SetCurrentSession(SessionCache.FirstOrDefault(x => x.Key != sessionId).Key);
- if (tokens.Remove(sessionId))
- await SaveStorage();
+ 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) {
@@ -136,16 +150,66 @@ public class RmuSessionStore(
}
}
+ 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 MigrateFromMRU();
+ if (!hasMigrated) {
+ await RunMigrations();
+ await LoadStorage(true);
+ }
else
logger.LogWarning("No sessions found in storage.");
+
return;
}
@@ -169,7 +233,8 @@ public class RmuSessionStore(
CurrentSession = currentSession;
}
- private async Task SaveStorage() {
+ 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,
@@ -177,6 +242,7 @@ public class RmuSessionStore(
)
);
await storageService.DataStorageProvider.SaveObjectAsync("rmu.session", CurrentSession?.SessionId);
+ if (log) logger.LogWarning("{count} sessions saved to storage.", SessionCache.Count);
}
#endregion
@@ -184,67 +250,70 @@ public class RmuSessionStore(
#region Migrations
public async Task RunMigrations() {
- await LoadStorage();
- await MigrateFromMRU();
+ await MigrateFromMru();
await MigrateAccountsToKeyedStorage();
}
- private async Task MigrateFromMRU() {
- await LoadStorage();
- logger.LogInformation("Migrating from MRU token namespace!");
+ private async Task MigrateFromMru() {
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("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("rmu.tokens", oldTokens);
- await dsp.DeleteObjectAsync("tokens");
+ 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.tokens")) {
- var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("mru.tokens");
- if (oldTokens != null) {
- await dsp.SaveObjectAsync("rmu.tokens", oldTokens);
- await dsp.DeleteObjectAsync("mru.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() {
- await LoadStorage();
- logger.LogInformation("Migrating accounts to keyed storage!");
var dsp = storageService.DataStorageProvider!;
- if (await dsp.ObjectExistsAsync("rmu.tokens")) {
- var tokens = await dsp.LoadObjectAsync<JsonNode>("rmu.tokens") ?? throw new Exception("Failed to load tokens");
- if (tokens is JsonArray array) {
- var keyedTokens = array
- .Deserialize<UserAuth[]>()!
- .ToDictionary(x => x.GetHashCode().ToString(), x => x);
- await dsp.SaveObjectAsync("rmu.sessions", keyedTokens);
- await dsp.DeleteObjectAsync("rmu.tokens");
- }
- }
+ 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 tokens");
- var sessionId = (await GetAllSessions())
- .FirstOrDefault(x => x.Value.Equals(token)).Key;
+ 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 not null) {
- await dsp.SaveObjectAsync("rmu.session", sessionId);
- }
- else AddSession(token);
+ 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
diff --git a/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs b/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
index 7078308..5c0fdab 100644
--- a/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
+++ b/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
@@ -12,7 +12,7 @@ public class DefaultRoomCreationTemplate : IRoomCreationTemplate {
new() {
Name = "My new room",
RoomAliasName = "myroom",
- InitialState = new List<StateEvent> {
+ InitialState = new List<MatrixEvent> {
new() {
Type = "m.room.history_visibility",
TypedContent = new RoomHistoryVisibilityEventContent() {
@@ -82,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/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 aa9f37f..9100c87 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>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LinkIncremental>true</LinkIncremental>
@@ -10,40 +10,44 @@
<UseBlazorWebAssembly>true</UseBlazorWebAssembly>
<BlazorEnableCompression>false</BlazorEnableCompression>
+ <CompressionEnabled>false</CompressionEnabled>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
<BlazorCacheBootResources>false</BlazorCacheBootResources>
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
+ <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
+ <WasmEnableHotReload>false</WasmEnableHotReload>
</PropertyGroup>
<!-- 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>-->
+ <!-- <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>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0"/>
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0"/>
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" />
- <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.2" />
- <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.1" />
- <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.5.39" />
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0"/>
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" PrivateAssets="all"/>
+ <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.0"/>
+ <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="10.0.0"/>
+ <PackageReference Include="SpawnDev.BlazorJS" Version="2.47.0"/>
+ <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.26.0"/>
</ItemGroup>
<ItemGroup>
@@ -52,8 +56,8 @@
</ItemGroup>
<ItemGroup>
-<!-- <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'"/>-->
+ <!-- <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>
diff --git a/MatrixUtils.Web/Pages/Dev/DevOptions.razor b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
index 33e577f..281cf07 100644
--- a/MatrixUtils.Web/Pages/Dev/DevOptions.razor
+++ b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
@@ -21,7 +21,7 @@
</p>
<details>
<summary>Manage local sessions</summary>
-
+
</details>
@if (userSettings is not null) {
@@ -40,10 +40,14 @@
@code {
private RmuSessionStore.Settings? userSettings { get; set; }
+
protected override async Task OnInitializedAsync() {
- // userSettings = await TieredStorage.DataStorageProvider.LoadObjectAsync<RmuSessionStore.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() {
@@ -58,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");
}
@@ -69,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 3b2d533..f6392a4 100644
--- a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
+++ b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
@@ -1,5 +1,8 @@
@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>
@@ -14,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>
@@ -61,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
@@ -69,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/WellKnownRes.razor b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
index 1906dd8..722f9b3 100644
--- a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
+++ b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
@@ -8,7 +8,7 @@
<h3>Known Homeserver List</h3>
<hr/>
-<span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClick="@Execute">Execute</LinkButton></span>
+<span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClickAsync="@Execute">Execute</LinkButton></span>
<span>Stats:</span><br/>
<span>Server count: @entries.Count</span><br/>
@@ -72,6 +72,7 @@
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 } };
+ public bool HasPolicyServerWellKnown => WellKnownResolutionResult?.PolicyServerWellKnown?.Content is not null and not { PublicKey: null or "" };
}
private async Task Execute() {
diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
index e1b46e2..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/>
@@ -11,7 +12,9 @@ else {
<h4>Synapse tools</h4>
<hr/>
<a href="/HSAdmin/Synapse/RoomQuery">Query rooms</a><br/>
- <a href="/HSAdmin/Synapse/BlockMedia">Block media</a>
+ <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>
diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
index 87600c6..ec2ec54 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
@@ -3,7 +3,7 @@
@using LibMatrix.Responses
<h3>Manage external profiles</h3>
-<LinkButton OnClick="AddAllLocalProfiles">Add local sessions</LinkButton>
+<LinkButton OnClickAsync="AddAllLocalProfiles">Add local sessions</LinkButton>
@foreach(var p in ExternalProfiles)
{
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
index d07ff08..5ccaca9 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
@@ -3,6 +3,7 @@
@using ArcaneLibs.Extensions
@using LibMatrix
@using LibMatrix.EventTypes.Spec
+@using LibMatrix.StructuredData
<h3>Homeserver Administration - Block media</h3>
@if (Homeserver is not null) {
@@ -24,13 +25,13 @@
<pre>@MxcUri?.ToJson(ignoreNull: true)</pre>
@if (Event is not null) {
- <LinkButton OnClick="@RedactAllEvents">Redact all messages</LinkButton>
+ <LinkButton OnClickAsync="@RedactAllEvents">Redact all messages</LinkButton>
}
@if (Event?.Sender?.Split(':', 2)[1] == Homeserver?.ServerName) {
<p>User is a local user!</p>
- <LinkButton OnClick="@DeactivateUser">Deactivate User</LinkButton>
- <LinkButton OnClick="@QuarantineMediaByUser">Quarantine all media</LinkButton>
+ <LinkButton OnClickAsync="@DeactivateUser">Deactivate User</LinkButton>
+ <LinkButton OnClickAsync="@QuarantineMediaByUser">Quarantine all media</LinkButton>
}
}
@@ -95,7 +96,7 @@
}
}
- private StateEventResponse? Event { get; set; }
+ private MatrixEventResponse? Event { get; set; }
private string? EventJson {
get;
@@ -139,7 +140,7 @@
private async Task ExpandEventJson() {
Console.WriteLine("Expanding event JSON...");
if (!string.IsNullOrWhiteSpace(EventJson)) {
- Event = JsonSerializer.Deserialize<StateEventResponse>(EventJson);
+ Event = JsonSerializer.Deserialize<MatrixEventResponse>(EventJson);
MxcUrl = Event?.ContentAs<RoomMessageEventContent>()?.Url;
Console.WriteLine($"MXC URL: {MxcUrl}");
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/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
index d5daf75..b0e6a89 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -1,36 +1,12 @@
+@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)) {
- <b>Media options</b>
- <br/>
- <hr/>
- <span>Quarantine local media: </span>
- <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalMedia"/>
- <br/>
- <span>Quarantine remote media: </span>
- <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineRemoteMedia"/>
- <br/>
- <span>Delete remote media: </span>
- <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteRemoteMedia"/>
- <br/>
-
- <b>User options</b>
- <br/>
- <hr/>
- <span>Suspend local users: </span>
- <InputCheckbox @bind-Value="@Context.ExtraOptions.SuspendLocalUsers"></InputCheckbox>
- <br/>
- <span>Quarantine <b>ALL</b> local user media: </span>
- <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalUserMedia"></InputCheckbox>
- <br/>
- <span>Delete <b>ALL</b> local user media: </span>
- <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteLocalUserMedia"></InputCheckbox>
- <br/>
-
- <b>Room deletion options</b>
- <br/>
- <hr/>
+@if (string.IsNullOrWhiteSpace(Context.DeleteId) || EditorOnly) {
<span>Block room: </span>
<InputCheckbox @bind-Value="@Context.DeleteRequest.Block"/>
<br/>
@@ -40,19 +16,63 @@
<span>Force purge room (unsafe): </span>
<InputCheckbox @bind-Value="@Context.DeleteRequest.ForcePurge"></InputCheckbox>
<br/>
- <span>Warning room User ID (optional): </span>
- <FancyTextBox @bind-Value="@Context.DeleteRequest.NewRoomUserId"/>
- <br/>
- @if (!string.IsNullOrWhiteSpace(Context.DeleteRequest.NewRoomUserId)) {
+ <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>
- <LinkButton OnClick="@DeleteRoom">Execute</LinkButton>
+ @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 {
@@ -63,14 +83,52 @@
[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();
@@ -80,11 +138,14 @@
ForcePurge = false
};
+ public SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom? RoomDetails { get; set; }
+
public class ExtraDeleteOptions {
- // room options
+ public bool FollowTombstone { get; set; }
+
+ // media options
public bool QuarantineLocalMedia { get; set; }
public bool QuarantineRemoteMedia { get; set; }
-
public bool DeleteRemoteMedia { get; set; }
// user options
@@ -95,9 +156,14 @@
}
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.DeleteId!);
+ await TaskMap.RemoveValueAsync(Context.RoomId!);
+ }
+ catch (Exception e) {
+ Console.WriteLine("Failed to remove completed room shutdown task from map: " + e);
}
finally {
OnCompleteLock.Release();
@@ -105,9 +171,96 @@
}
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<MatrixEventResponse?> 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
index 79e7357..05899c8 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -1,10 +1,18 @@
@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.Requests
@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>
@@ -16,121 +24,154 @@
<option value="@item.Key">@item.Value</option>
}
</select><br/>
-<label>Ascending: </label>
-<InputCheckbox @bind-Value="Ascending"/><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>
- <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>
+ <summary>Local filtering (slow)</summary>
+ <SynapseRoomQueryFilter Filter="@Filter"/>
</details>
-<button class="btn btn-primary" @onclick="Search">Search</button>
+<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>
- <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;">
+@foreach (var room in Results) {
+ <div class="room-list-item">
@* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@
<p>
- @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) {
- <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span>
- <br/>
+ @if (EnableMultiPurge) {
+ <InputCheckbox @bind-Value="@room.MultiPurgeSelected"/>
+ <span> </span>
}
- else {
- <span>@res.RoomId (@res.Name)</span>
- <br/>
+ @if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) {
+ <span>@room.CanonicalAlias - </span>
}
- @if (!string.IsNullOrWhiteSpace(res.Creator)) {
- @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@
- <span>Created by @res.Creator</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 OnClick="@(() => {
- DeleteRequests.Add(res.RoomId, new() {
- RoomId = res.RoomId,
- DeleteRequest = new() {
- Block = true,
- Purge = true,
- ForcePurge = false
- }
- });
-
- return Task.CompletedTask;
- })">Delete room
- </LinkButton>
+ <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>
- <span>@res.StateEvents state events</span><br/>
- @if (res.LocalMembers is null) {
- <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span>
+
+ @{
+ 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>
}
- else {
- <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server: @(string.Join(", ", res.LocalMembers))</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>
}
- <details>
- <summary>Full result data</summary>
- <pre>@res.ToJson(ignoreNull: true)</pre>
- </details>
</div>
}
@* *@
@@ -146,48 +187,15 @@
@* </ModalWindow> *@
@* } *@
-@foreach(var (roomId, deleteRequest) in DeleteRequests) {
- <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/>
+@foreach (var (roomId, deleteRequest) in DeleteRequests) {
+ <ModalWindow Title="@($"Delete room {roomId}")" OnCloseClicked="@(() => {
+ DeleteRequests.Remove(roomId);
+ StateHasChanged();
+ })">
+ <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/>
+ </ModalWindow>
}
-<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]
@@ -196,27 +204,65 @@
[Parameter]
[SupplyParameterFromQuery(Name = "name_search")]
- public string SearchTerm { get; set; }
+ public string? SearchTerm { get; set; }
[Parameter]
[SupplyParameterFromQuery(Name = "ascending")]
- public bool Ascending { get; set; }
+ 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 string Status { get; set; }
-
- public SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new();
+ 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() {
- if (Ascending == null)
- Ascending = true;
OrderBy ??= "name";
var execute = false;
@@ -224,67 +270,95 @@
foreach (var (key, value) in QueryHelpers.ParseQuery(new Uri(NavigationManager.Uri).Query)) {
switch (key) {
case "RoomIdContains":
- Filter.RoomIdContains = value[0]!;
+ Filter.RoomId.Enabled = Filter.RoomId.CheckValueContains = true;
+ Filter.RoomId.ValueContains = value[0]!;
break;
case "NameContains":
- Filter.NameContains = value[0]!;
+ Filter.Name.Enabled = Filter.Name.CheckValueContains = true;
+ Filter.Name.ValueContains = value[0]!;
break;
case "CanonicalAliasContains":
- Filter.CanonicalAliasContains = value[0]!;
+ Filter.CanonicalAlias.Enabled = Filter.CanonicalAlias.CheckValueContains = true;
+ Filter.CanonicalAlias.ValueContains = value[0]!;
break;
case "VersionContains":
- Filter.VersionContains = value[0]!;
+ Filter.Version.Enabled = Filter.Version.CheckValueContains = true;
+ Filter.Version.ValueContains = value[0]!;
break;
case "CreatorContains":
- Filter.CreatorContains = value[0]!;
+ Filter.Creator.Enabled = Filter.Creator.CheckValueContains = true;
+ Filter.Creator.ValueContains = value[0]!;
break;
case "EncryptionContains":
- Filter.EncryptionContains = value[0]!;
+ Filter.Encryption.Enabled = Filter.Encryption.CheckValueContains = true;
+ Filter.Encryption.ValueContains = value[0]!;
break;
case "JoinRulesContains":
- Filter.JoinRulesContains = value[0]!;
+ Filter.JoinRules.Enabled = Filter.JoinRules.CheckValueContains = true;
+ Filter.JoinRules.ValueContains = value[0]!;
break;
case "GuestAccessContains":
- Filter.GuestAccessContains = value[0]!;
+ Filter.GuestAccess.Enabled = Filter.GuestAccess.CheckValueContains = true;
+ Filter.GuestAccess.ValueContains = value[0]!;
break;
case "HistoryVisibilityContains":
- Filter.HistoryVisibilityContains = value[0]!;
+ Filter.HistoryVisibility.Enabled = Filter.HistoryVisibility.CheckValueContains = true;
+ Filter.HistoryVisibility.ValueContains = value[0]!;
break;
case "Federatable":
- Filter.Federatable = bool.Parse(value[0]!);
- Filter.CheckFederation = true;
+ Filter.Federation = new() {
+ Enabled = true,
+ Value = bool.Parse(value[0]!)
+ };
break;
case "Public":
- Filter.Public = value[0] == "true";
- Filter.CheckPublic = true;
+ Filter.Public = new() {
+ Enabled = true,
+ Value = bool.Parse(value[0]!)
+ };
break;
case "JoinedMembersGreaterThan":
- Filter.JoinedMembersGreaterThan = int.Parse(value[0]!);
+ Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true;
+ Filter.JoinedMembers.GreaterThan = int.Parse(value[0]!);
break;
case "JoinedMembersLessThan":
- Filter.JoinedMembersLessThan = int.Parse(value[0]!);
+ Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true;
+ Filter.JoinedMembers.LessThan = int.Parse(value[0]!);
break;
case "JoinedLocalMembersGreaterThan":
- Filter.JoinedLocalMembersGreaterThan = int.Parse(value[0]!);
+ Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true;
+ Filter.JoinedLocalMembers.GreaterThan = int.Parse(value[0]!);
break;
case "JoinedLocalMembersLessThan":
- Filter.JoinedLocalMembersLessThan = int.Parse(value[0]!);
+ Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true;
+ Filter.JoinedLocalMembers.LessThan = int.Parse(value[0]!);
break;
case "StateEventsGreaterThan":
- Filter.StateEventsGreaterThan = int.Parse(value[0]!);
+ Filter.StateEvents.Enabled = Filter.StateEvents.CheckGreaterThan = true;
+ Filter.StateEvents.GreaterThan = int.Parse(value[0]!);
break;
case "StateEventsLessThan":
- Filter.StateEventsLessThan = int.Parse(value[0]!);
+ 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();
@@ -293,109 +367,96 @@
private async Task Search() {
Results.Clear();
- var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
- if (hs is AuthenticatedHomeserverSynapse synapse) {
- Homeserver = synapse;
- var searchRooms = synapse.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator();
- while (await searchRooms.MoveNextAsync()) {
- var room = searchRooms.Current;
+ 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;
- if (Results.Count < 100)
- Console.WriteLine("Hit: " + room.ToJson(false));
+ var roomInfo = new RoomInfo {
+ RoomId = room.RoomId,
+ Name = room.Name,
+ CanonicalAlias = room.CanonicalAlias,
+ Creator = room.Creator,
+ Version = room.Version,
+ Encryption = room.Encryption,
+ Federatable = room.Federatable,
+ Public = room.Public,
+ JoinRules = room.JoinRules,
+ GuestAccess = room.GuestAccess,
+ HistoryVisibility = room.HistoryVisibility,
+ StateEvents = room.StateEvents,
+ JoinedMembers = room.JoinedMembers,
+ JoinedLocalMembers = room.JoinedLocalMembers,
+ OriginHomeserver =
+ Homeserver.GetRoom(room.RoomId).IsV12PlusRoomId
+ ? room.RoomId.Split(':', 2).Skip(1).FirstOrDefault(string.Empty)
+ : string.Empty
+ };
- var roomInfo = new RoomInfo {
- RoomId = room.RoomId,
- Name = room.Name,
- CanonicalAlias = room.CanonicalAlias,
- Creator = room.Creator,
- Version = room.Version,
- Encryption = room.Encryption,
- Federatable = room.Federatable,
- Public = room.Public,
- JoinRules = room.JoinRules,
- GuestAccess = room.GuestAccess,
- HistoryVisibility = room.HistoryVisibility,
- StateEvents = room.StateEvents,
- JoinedMembers = room.JoinedMembers,
- JoinedLocalMembers = room.JoinedLocalMembers
- };
-
- Results.Add(roomInfo);
+ if (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)}");
+ }
+ }
- if (room.JoinedLocalMembers is > 0 and < 100)
- roomInfo.LocalMembers = (await synapse.Admin.GetRoomMembersAsync(room.RoomId)).Members.Where(x => x.EndsWith(":" + synapse.ServerName)).ToList();
+ Results.Add(roomInfo);
- if (Results.Count < 200 || Results.Count % 1000 == 0) {
- StateHasChanged();
- await Task.Yield();
- }
+ 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 async Task DeleteRoom() {
- // if (DeleteRequest is { } deleteRequest) {
- // var media = await Homeserver.Admin.GetRoomMediaAsync(deleteRequest.RoomId);
- // if (deleteRequest.DeleteRequest.QuarantineRemoteMedia) {
- // foreach (var remoteMedia in media.Remote) {
- // await Homeserver.Admin.QuarantineMediaById(remoteMedia);
- // }
- // }
- //
- // if (deleteRequest.DeleteRequest.DeleteRemoteMedia) {
- // foreach (var remoteMedia in media.Remote) {
- // await Homeserver.Admin.DeleteMediaById(remoteMedia);
- // }
- // }
- // else if (deleteRequest.DeleteRequest.QuarantineLocalMedia) {
- // foreach (var localMedia in media.Local) {
- // await Homeserver.Admin.QuarantineMediaById(localMedia);
- // }
- // }
- //
- // var deleteId = await Homeserver.Admin.DeleteRoom(deleteRequest.RoomId, deleteRequest.DeleteRequest, waitForCompletion: false);
- // DeleteRequest = null;
- // List<string> alreadyCleanedUsers = [];
- // while (true) {
- // var status = await Homeserver.Admin.GetRoomDeleteStatus(deleteId.DeleteId);
- // DeleteStatuses[deleteRequest.RoomId] = status;
- // StateHasChanged();
- // await Task.Delay(5000);
- // if (status.Status == "complete") {
- // DeleteStatuses.Remove(deleteRequest.RoomId);
- // StateHasChanged();
- // break;
- // }
- //
- // if (status.Status == "failed") {
- // deleteId = await Homeserver.Admin.DeleteRoom(deleteRequest.RoomId, deleteRequest.DeleteRequest, waitForCompletion: false);
- // }
- //
- // var newCleanedUsers = status.ShutdownRoom?.KickedUsers?.Except(alreadyCleanedUsers).ToList();
- // if (newCleanedUsers is not null) {
- // alreadyCleanedUsers.AddRange(newCleanedUsers);
- // foreach (var user in newCleanedUsers) {
- // if (deleteRequest.DeleteRequest.SuspendLocalUsers) {
- // // await Homeserver.Admin.(user);
- // }
- //
- // if (deleteRequest.DeleteRequest.QuarantineLocalUserMedia) {
- // await Homeserver.Admin.QuarantineMediaByUserId(user);
- // }
- //
- // if (deleteRequest.DeleteRequest.DeleteLocalUserMedia) {
- // var userMedia = Homeserver.Admin.GetUserMediaEnumerableAsync(user);
- // await foreach (var mediaEntry in userMedia) {
- // await Homeserver.Admin.DeleteMediaById(mediaEntry.MediaId);
- // }
- // }
- // }
- // }
- // }
- // }
- // }
private readonly Dictionary<string, string> validOrderBy = new() {
{ "name", "Room name" },
@@ -415,6 +476,143 @@
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
index e69de29..62941e5 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css
+++ 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..3cc5a6a
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor
@@ -0,0 +1,211 @@
+@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<MatrixEventResponse>? 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/Index.razor b/MatrixUtils.Web/Pages/Index.razor
index 7de4613..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,7 +20,7 @@ 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.Auth;
@@ -58,9 +59,9 @@ Small collection of tools to do not-so-everyday things.
</td>
<td>
<p>
- <LinkButton OnClick="@(() => ManageUser(session.SessionId))">Manage</LinkButton>
- <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
- <LinkButton OnClick="@(() => RemoveUser(session.SessionId, 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>
@@ -89,7 +90,7 @@ Small collection of tools to do not-so-everyday things.
</p>
</td>
<td>
- <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
+ <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
</td>
</tr>
}
@@ -118,10 +119,10 @@ Small collection of tools to do not-so-everyday things.
</p>
</td>
<td>
- <LinkButton OnClick="@(() => Task.Run(() => NavigationManager.NavigateTo($"/InvalidSession?ctx={session.SessionId}")))">Re-login</LinkButton>
+ <LinkButton OnClickAsync="@(() => Task.Run(() => NavigationManager.NavigateTo($"/InvalidSession?ctx={session.SessionId}")))">Re-login</LinkButton>
</td>
<td>
- <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
+ <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton>
</td>
</tr>
}
@@ -137,6 +138,8 @@ Small collection of tools to do not-so-everyday things.
private const bool _debug = false;
#endif
+ private bool Busy { get; set; } = true;
+
private class HomepageSessionInfo : RmuSessionStore.SessionInfo {
public UserInfo? UserInfo { get; set; }
public ServerVersionResponse? ServerVersion { get; set; }
@@ -153,7 +156,6 @@ Small collection of tools to do not-so-everyday things.
protected override async Task OnInitializedAsync() {
Console.WriteLine("Index.OnInitializedAsync");
logger.LogDebug("Initialising index page");
- await sessionStore.RunMigrations();
_currentSession = await sessionStore.GetCurrentSession();
_sessions.Clear();
@@ -242,7 +244,9 @@ 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 {
diff --git a/MatrixUtils.Web/Pages/InvalidSession.razor b/MatrixUtils.Web/Pages/InvalidSession.razor
index 1ec99f6..f86d112 100644
--- a/MatrixUtils.Web/Pages/InvalidSession.razor
+++ b/MatrixUtils.Web/Pages/InvalidSession.razor
@@ -8,14 +8,14 @@
@if (_auth is not null) {
<p>It appears that the affected user is @_auth.UserId (@_auth.DeviceId) on @_auth.Homeserver!</p>
- <LinkButton OnClick="@(OpenRefreshDialog)">Refresh token</LinkButton>
- <LinkButton OnClick="@(RemoveUser)">Remove</LinkButton>
+ <LinkButton OnClickAsync="@(OpenRefreshDialog)">Refresh token</LinkButton>
+ <LinkButton OnClickAsync="@(RemoveUser)">Remove</LinkButton>
@if (_showRefreshDialog) {
<ModalWindow MinWidth="300" X="275" Y="300" Title="@($"Password for {_auth.UserId}")">
<FancyTextBox IsPassword="true" @bind-Value="@_password"></FancyTextBox>
<br/>
- <LinkButton OnClick="TryLogin">Log in</LinkButton>
+ <LinkButton OnClickAsync="TryLogin">Log in</LinkButton>
@if (_loginException is not null) {
<pre style="color: red;">@_loginException.RawContent</pre>
}
diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
index 8831dd1..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/>
diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs
index 16051b8..c58114e 100644
--- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs
+++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs
@@ -13,9 +13,10 @@ public class ClientSyncWrapper(AuthenticatedHomeserverGeneric homeserver) : Noti
MinimumDelay = TimeSpan.FromMilliseconds(2000),
IsInitialSync = false
};
+
private string _status = "Loading...";
- public ObservableCollection<StateEvent> AccountData { get; set; } = new();
+ public ObservableCollection<MatrixEvent> AccountData { get; set; } = new();
public ObservableCollection<RoomInfo> Rooms { get; set; } = new();
public string Status {
@@ -29,13 +30,12 @@ public class ClientSyncWrapper(AuthenticatedHomeserverGeneric homeserver) : Noti
Status = $"[{DateTime.Now:s}] Syncing...";
await foreach (var response in resp) {
Task.Yield();
- Status = $"[{DateTime.Now:s}] {response.Rooms?.Join?.Count ?? 0 + response.Rooms?.Invite?.Count ?? 0 + response.Rooms?.Leave?.Count ?? 0} rooms, {response.AccountData?.Events?.Count ?? 0} account data, {response.ToDevice?.Events?.Count ?? 0} to-device, {response.DeviceLists?.Changed?.Count ?? 0} device lists, {response.Presence?.Events?.Count ?? 0} presence updates";
+ Status =
+ $"[{DateTime.Now:s}] {response.Rooms?.Join?.Count ?? 0 + response.Rooms?.Invite?.Count ?? 0 + response.Rooms?.Leave?.Count ?? 0} rooms, {response.AccountData?.Events?.Count ?? 0} account data, {response.ToDevice?.Events?.Count ?? 0} to-device, {response.DeviceLists?.Changed?.Count ?? 0} device lists, {response.Presence?.Events?.Count ?? 0} presence updates";
await HandleSyncResponse(response);
await Task.Yield();
}
}
- private async Task HandleSyncResponse(SyncResponse resp) {
-
- }
+ private async Task HandleSyncResponse(SyncResponse resp) { }
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
index a974a8f..7199934 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
@@ -26,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>
@@ -78,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);
@@ -109,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;
@@ -118,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 b8eb257..ed65e94 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
@@ -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 dac9c49..686894c 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
@@ -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/Index2Components/RoomsIndex2ByRoomTypeTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
index 79f931b..dd217e9 100644
--- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
+++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
@@ -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/LoginPage.razor b/MatrixUtils.Web/Pages/LoginPage.razor
index 88577a2..38ede74 100644
--- a/MatrixUtils.Web/Pages/LoginPage.razor
+++ b/MatrixUtils.Web/Pages/LoginPage.razor
@@ -22,8 +22,8 @@
<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/>
@@ -44,7 +44,7 @@
<FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox>
</span>
<br/>
-<LinkButton OnClick="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton>
+<LinkButton OnClickAsync="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton>
<br/>
<br/>
@@ -101,7 +101,7 @@
}
</table>
<br/>
-<LinkButton OnClick="@LoginAll">Log in</LinkButton>
+<LinkButton OnClickAsync="@LoginAll">Log in</LinkButton>
@code {
diff --git a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
index e30adf6..17dd554 100644
--- a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
+++ b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
@@ -19,6 +19,7 @@ else {
else if (checkedRooms.Count > 1) {
<p>Done!</p>
}
+
@foreach (var (state, rooms) in matchingStates) {
<u>@state</u>
<br/>
@@ -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 021ad18..051d5af 100644
--- a/MatrixUtils.Web/Pages/Rooms/Create.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Create.razor
@@ -12,10 +12,10 @@
@* <pre Contenteditable="true" @onkeypress="@JsonChanged" content="JsonString">@JsonString</pre> *@
<style>
- table.table-top-first-tr tr td:first-child {
- vertical-align: top;
- }
- </style>
+ table.table-top-first-tr tr td:first-child {
+ vertical-align: top;
+ }
+</style>
<table class="table-top-first-tr">
<tr style="padding-bottom: 16px;">
<td>Preset:</td>
@@ -41,7 +41,10 @@
}
else {
<FancyTextBox @bind-Value="@creationEvent.Name"></FancyTextBox>
- <p>(#<FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox>:@Homeserver.WhoAmI.UserId.Split(':').Last())</p>
+ <p>(#
+ <FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox>
+ :@Homeserver.WhoAmI.UserId.Split(':').Last())
+ </p>
}
</td>
</tr>
@@ -89,7 +92,8 @@
<td>
@* <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/>
+ <FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox>
+ <br/>
<InputFile OnChange="RoomIconFilePicked"></InputFile>
</div>
</td>
@@ -105,19 +109,27 @@
<FancyTextBox Formatter="@GetPermissionFriendlyName"
Value="@_event"
ValueChanged="val => { creationEvent.PowerLevelContentOverride.Events.ChangeKey(_event, val); }">
- </FancyTextBox>:
+ </FancyTextBox>
+ :
</td>
<td>
- <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/>
+ <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]"
+ @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }"
+ @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/>
</td>
</tr>
}
@foreach (var user in creationEvent.PowerLevelContentOverride.Users.Keys) {
var _user = user;
<tr>
- <td><FancyTextBox Value="@_user" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>:</td>
<td>
- <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/>
+ <FancyTextBox Value="@_user"
+ ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>
+ :
+ </td>
+ <td>
+ <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]"
+ @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/>
</td>
</tr>
}
@@ -266,6 +278,7 @@
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")) {
@@ -299,7 +312,7 @@
private void InviteMember(string mxid) {
if (!creationEvent.InitialState.Any(x => x.Type == "m.room.member" && x.StateKey == mxid) && Homeserver.UserId != mxid)
- creationEvent.InitialState.Add(new StateEvent {
+ creationEvent.InitialState.Add(new MatrixEvent {
Type = "m.room.member",
StateKey = mxid,
TypedContent = new RoomMemberEventContent {
@@ -316,7 +329,7 @@
"m.room.server_acl" => "Server ACL",
"m.room.avatar" => "Avatar",
_ => key
- };
+ };
private string GetPermissionFriendlyName(string key) => key switch {
"m.reaction" => "Send reaction",
@@ -331,6 +344,6 @@
"m.room.pinned_events" => "Pin events",
"m.room.server_acl" => "Change server ACLs",
_ => key
- };
+ };
}
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 0373a46..115c903 100644
--- a/MatrixUtils.Web/Pages/Rooms/Index.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Index.razor
@@ -73,7 +73,7 @@
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 ?? []) {
@@ -97,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,
@@ -106,9 +107,9 @@
// };
// profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId);
- RunSyncLoop(syncHelper);
+ _ = RunSyncLoop(syncHelper);
// RunSyncLoop(profileSyncHelper);
- RunQueueProcessor();
+ _ = RunQueueProcessor();
await base.OnInitializedAsync();
}
@@ -138,7 +139,7 @@
}
else {
// Console.WriteLine($"QueueWorker: encountered new room {roomId}!");
- room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.State?.Events);
+ room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.StateAfter?.Events);
Rooms.Add(room);
}
@@ -147,11 +148,10 @@
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}");
@@ -181,7 +181,8 @@
get => _status;
set {
_status = value;
- StateHasChanged();
+ // StateHasChanged();
+ Console.WriteLine(value);
}
}
@@ -191,7 +192,8 @@
get => _status2;
set {
_status2 = value;
- StateHasChanged();
+ // StateHasChanged();
+ Console.WriteLine(value);
}
}
@@ -203,26 +205,24 @@
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));
@@ -231,6 +231,7 @@
$"{sync.Rooms?.Join?.Count ?? 0} new updates!";
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 9c35673..f2ab186 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -5,159 +5,148 @@
@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.Common
@using LibMatrix.EventTypes.Interop.Draupnir
@using LibMatrix.EventTypes.Spec.State.RoomInfo
-
-@using MatrixUtils.Web.Shared.PolicyEditorComponents
@using SpawnDev.BlazorJS.WebWorkers
+@using MatrixUtils.Web.Pages.Rooms.PolicyListComponents
+@using SpawnDev.BlazorJS
@inject WebWorkerService WebWorkerService
+@inject ILogger<PolicyList> logger
+@inject BlazorJSRuntime JsRuntime
-<h3>Policy list editor - Editing @(RoomName ?? RoomId)</h3>
-@if (!string.IsNullOrWhiteSpace(DraupnirShortcode)) {
- <span style="margin-right: 2em;">Shortcode: @DraupnirShortcode</span>
-}
-@if (!string.IsNullOrWhiteSpace(RoomAlias)) {
- <span>Alias: @RoomAlias</span>
-}
-<hr/>
-@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@
-<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton>
-<LinkButton OnClick="@(() => { MassCreatePolicies = true; return Task.CompletedTask; })">Create many new policies</LinkButton>
-
-@if (Loading) {
- <p>Loading...</p>
-}
-else if (PolicyEventsByType is not { Count: > 0 }) {
- <p>No policies yet</p>
+@if (!IsInitialised) {
+ <p>Connecting to homeserver...</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>
+ <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>
+ }
- Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}");
+ @if (DuplicateBans?.ActivePolicies.Count > 0) {
+ <p style="color: orange;">
+ Found @DuplicateBans.Value.ActivePolicies.Count duplicate bans
+ </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">
- @{
- 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();
+ @if (RedundantBans?.ActivePolicies.Count > 0) {
+ <p style="color: orange;">
+ Found @RedundantBans.Value.ActivePolicies.Count redundant bans
+ </p>
+ }
- var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
- .Where(x => props.Any(y => y.Name == x.Name))
- .ToFrozenSet();
- Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
- }
- <thead>
- <tr>
- @foreach (var name in propNames) {
- <th>@name</th>
- }
- <th>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!;
- }
- @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>
- }
+ // logger.LogInformation($"Rendered header in {renderSw.GetElapsedAndRestart()}");
- @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.Type)) {
- <LinkButton OnClick="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent</LinkButton>
- @if (CurrentUserIsDraupnir) {
- <LinkButton Color="@(ActiveKicks.ContainsKey(policy) ? "#FF0000" : null)" OnClick="@(() => DraupnirKickMatching(policy))">Kick users @(ActiveKicks.ContainsKey(policy) ? $"({ActiveKicks[policy]})" : null)</LinkButton>
- }
- }
- }
- else {
- <p>No permission to modify</p>
- }
- </div>
- </td>
- </tr>
- }
- </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>
- }
+ // 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()}");
- Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
- Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}");
-}
+ @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>
+ }
-@if (ServerPolicyToMakePermanent is not null) {
- <ModalWindow Title="Make policy permanent">
+ foreach (var collection in PolicyCollections.Values.OrderByDescending(x => x.ActivePolicies.Count)) {
+ <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent>
+ }
- </ModalWindow>
-}
+ // 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> *@
+ // }
-@if (MassCreatePolicies) {
- <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => { MassCreatePolicies = false; LoadStatesAsync(); })"></MassPolicyEditorModal>
+ // logger.LogInformation($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
+ logger.LogInformation("Rendered in {TimeSpan}", renderTotalSw.Elapsed);
+ }
}
@code {
@@ -168,50 +157,50 @@ else {
private const bool Debug = false;
#endif
+ private bool IsInitialised { get; set; } = false;
private bool Loading { get; set; } = true;
[Parameter]
- public string RoomId { get; set; }
+ public required string RoomId { get; set; }
- private bool _enableAvatars;
- private StateEventResponse? _currentlyEditingEvent;
- private bool _massCreatePolicies;
- private StateEventResponse? _serverPolicyToMakePermanent;
-
- private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
-
- private StateEventResponse? CurrentlyEditingEvent {
- get => _currentlyEditingEvent;
+ [Parameter, SupplyParameterFromQuery]
+ public bool RenderEventInfo {
+ get;
set {
- _currentlyEditingEvent = value;
+ field = value;
StateHasChanged();
}
}
- public StateEventResponse? ServerPolicyToMakePermanent {
- get => _serverPolicyToMakePermanent;
+ private Dictionary<Type, List<MatrixEventResponse>> PolicyEventsByType { get; set; } = new();
+
+ public MatrixEventResponse? ServerPolicyToMakePermanent {
+ get;
set {
- _serverPolicyToMakePermanent = value;
+ field = value;
StateHasChanged();
}
}
- private AuthenticatedHomeserverGeneric Homeserver { get; set; }
- private GenericRoom Room { get; set; }
- private RoomPowerLevelEventContent PowerLevels { get; set; }
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!;
+ private GenericRoom Room { get; set; } = null!;
+ private RoomPowerLevelEventContent PowerLevels { get; set; } = null!;
public bool CurrentUserIsDraupnir { get; set; }
- public string? RoomName { get; set; }
- public string? RoomAlias { get; set; }
- public string? DraupnirShortcode { get; set; }
- public Dictionary<StateEventResponse, int> ActiveKicks { get; set; } = [];
- public bool MassCreatePolicies {
- get => _massCreatePolicies;
- set {
- _massCreatePolicies = value;
- StateHasChanged();
- }
- }
+ public Dictionary<MatrixEventResponse, int> ActiveKicks { get; set; } = [];
+
+ private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.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();
@@ -219,202 +208,456 @@ else {
Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!;
if (Homeserver is null) return;
Room = Homeserver.GetRoom(RoomId!);
- await Task.WhenAll([
+ IsInitialised = true;
+ StateHasChanged();
+ await Task.WhenAll(
Task.Run(async () => { PowerLevels = (await Room.GetPowerLevelsAsync())!; }),
- Task.Run(async () => { DraupnirShortcode = (await Room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode; }),
- Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }),
- Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); }),
- Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>("org.matrix.mjolnir.protected_rooms")) is not null; }),
- ]);
- await LoadStatesAsync();
- Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
+ 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() {
- 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);
+ 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 (MatrixEvent.Equals(collection.RemovedPolicies[key].Policy, evt)) continue;
+ collection.RemovedPolicies[key] = policyInfo;
+ }
+ }
+ else {
+ collection.RemovedPolicies.Remove(key);
+ if (!collection.ActivePolicies.TryAdd(key, policyInfo)) {
+ if (MatrixEvent.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);
- private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
+ // return;
+ logger.LogInformation("LoadStatesAsync: Scanning for redundant policies...");
- private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
- .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+ 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;
- private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
- .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+ // foreach (var (policyInfo, policyContent) in allPolicies) {
+ // foreach (var (otherPolicyInfo, otherPolicyContent) in allPolicies) {
+ // if (policyInfo.Policy == otherPolicyInfo.Policy) continue; // same event
+ // if (MatrixEvent.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);
+ // }
+ // }
- private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
- ?? type.GetCustomAttributes<MatrixEventAttribute>()
- .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName;
+ int scanningPolicyCount = 0;
+ var aggregatedPolicies = PolicyCollections.Values
+ .Aggregate(new List<MatrixEventResponse>(), (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())));
- private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
+ 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 => MatrixEvent.Equals(x, y))).ToList();
+ original.MadeRedundantBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.MadeRedundantBy.Any(y => MatrixEvent.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);
+ }
- private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
- await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { });
- PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
- await LoadStatesAsync();
+ 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();
}
- private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
- await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent);
- CurrentlyEditingEvent = null;
- await LoadStatesAsync();
+ [return: WorkerTransfer]
+ private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(SpawnDev.BlazorJS.JSObjects.String policiesJson, int start, int end) {
+ var policies = JsonSerializer.Deserialize<List<MatrixEventResponse>>(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);
}
- private async Task UpgradePolicyAsync(StateEventResponse policyEvent) {
- policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type;
- await LoadStatesAsync();
+ [return: WorkerTransfer]
+ private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(string policiesJson, int start, int end) {
+ var policies = JsonSerializer.Deserialize<List<MatrixEventResponse>>(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);
}
- private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+ [return: WorkerTransfer]
+ private static Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<MatrixEventResponse> policies, int start, int end)
+ => CheckDuplicatePoliciesAsync(policies, start .. end);
- // event types, unnamed
- private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
- .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+ [return: WorkerTransfer]
+ private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<MatrixEventResponse> 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>();
- private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes
- .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray());
+ foreach (var (policyEvent, policyContent) in toCheck) {
+ List<MatrixEventResponse> duplicatedBy = [];
+ List<MatrixEventResponse> madeRedundantBy = [];
-#region Draupnir interop
+ foreach (var (otherPolicyEvent, otherPolicyContent) in allPolicies) {
+ if (policyEvent == otherPolicyEvent) continue; // same event
+ if (MatrixEvent.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
+ }
- private SemaphoreSlim ss = new(16, 16);
+ // 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);
+ }
+ }
- private async Task DraupnirKickMatching(StateEventResponse policy) {
- try {
- var content = policy.TypedContent! as PolicyRuleEventContent;
- if (content is null) return;
- if (string.IsNullOrWhiteSpace(content.Entity)) return;
+ 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
+ });
+ }
- var data = await Homeserver.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId);
- var rooms = data.Rooms.Select(Homeserver.GetRoom).ToList();
+ // await Task.Delay(1);
+ }
- ActiveKicks.Add(policy, rooms.Count);
- StateHasChanged();
- await Task.Delay(500);
+ await jsConsole.Info($"Worker: Found {modifiedPolicies.Count} modified policies in range {range} (length: {range.GetOffsetAndLength(policies.Count).Length}) in {sw.Elapsed}");
- // for (int i = 0; i < 12; i++) {
- // _ = WebWorkerService.TaskPool.Invoke(WasteCpu);
- // }
+ 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();
+ var states = await Room.GetFullStateAsListAsync();
+ // PolicyEventsByType.Clear();
- // 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);
- // }
+ logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed);
- 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);
+ foreach (var type in KnownPolicyTypes) {
+ if (!PolicyEventsByType.ContainsKey(type))
+ PolicyEventsByType.Add(type, new List
+ <MatrixEventResponse>(16000));
}
- }
-#region Nasty, nasty internals, please ignore!
+ 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;
+ e1 = _spsw.Elapsed;
+ var targetPolicies = PolicyEventsByType[state.MappedType];
+ e2 = _spsw.Elapsed;
+ if (!firstLoad && targetPolicies.FirstOrDefault(x => MatrixEvent.TypeKeyPairMatches(x, state)) is { } evt) {
+ e3 = _spsw.Elapsed;
+ if (MatrixEvent.Equals(evt, state)) {
+ if (count % 100 == 0) {
+ await Task.Delay(10);
+ await Task.Yield();
+ }
- 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);
+ 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);
}
- catch (Exception e) {
- Console.WriteLine(e);
+ 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();
}
- 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);
- }
+ 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 List<MatrixEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
+
+ // private List<MatrixEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ // .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+ //
+ // private List<MatrixEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ // .Where(x => x.RawContent is { Count: > 0 } && string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+ //
+ // private List<MatrixEventResponse> 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>()
+ .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName;
+
+ private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
+
+ public struct PolicyCollection {
+ public required string Name { get; init; }
+ public SpecialViewType ViewType { get; init; }
+ public int TotalCount => ActivePolicies.Count + RemovedPolicies.Count;
+
+ public required Dictionary<(string Type, string StateKey), PolicyInfo> ActivePolicies { get; set; }
+
+ // public Dictionary<(string Type, string StateKey), MatrixEventResponse> InvalidPolicies { get; set; }
+ public required Dictionary<(string Type, string StateKey), PolicyInfo> RemovedPolicies { get; set; }
+ public required FrozenDictionary<string, PropertyInfo> PropertiesToDisplay { get; set; }
+
+ public class PolicyInfo {
+ public required MatrixEventResponse Policy { get; init; }
+ public required List<MatrixEventResponse> MadeRedundantBy { get; set; }
+ public required List<MatrixEventResponse> DuplicatedBy { get; set; }
}
- private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, StateEventResponse policy) {
- Console.WriteLine($"Checking {roomId}...");
- Console.WriteLine(policy.EventId);
+ public enum SpecialViewType {
+ None,
+ Duplicates,
+ Redundant,
}
}
-#endregion
-
-#endregion
+ // 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..6f45041
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs
@@ -0,0 +1,142 @@
+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(MatrixEventResponse 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, MatrixEventResponse 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, MatrixEventResponse policy) {
+ Console.WriteLine($"Checking {roomId}...");
+ Console.WriteLine(policy.EventId);
+ }
+ }
+
+#endregion
+
+#endregion
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css
deleted file mode 100644
index afe9fb0..0000000
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css
+++ /dev/null
@@ -1,9 +0,0 @@
-th {
- border-width: 1px;
-}
-
-table {
- width: fit-content;
- border-width: 1px;
- vertical-align: middle;
-}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
index 982fc5a..ac918a8 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
@@ -15,7 +15,11 @@
<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>
+<LinkButton OnClickAsync="@(() => {
+ CurrentlyEditingEvent = new() { Type = "", RawContent = new() };
+ return Task.CompletedTask;
+ })">Create new policy
+</LinkButton>
@if (Loading) {
<p>Loading...</p>
@@ -71,16 +75,24 @@ else {
}
<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>
+ <LinkButton OnClickAsync="@(() => {
+ CurrentlyEditingEvent = policy;
+ return Task.CompletedTask;
+ })">Edit
+ </LinkButton>
+ <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Remove</LinkButton>
@if (policy.IsLegacyType) {
- <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
+ <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
}
@if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.EventId)) {
- <LinkButton OnClick="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent (wildcard)</LinkButton>
+ <LinkButton OnClickAsync="@(() => {
+ ServerPolicyToMakePermanent = policy;
+ return Task.CompletedTask;
+ })">Make permanent (wildcard)
+ </LinkButton>
@if (CurrentUserIsDraupnir) {
- <LinkButton OnClick="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton>
+ <LinkButton OnClickAsync="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton>
}
}
else {
@@ -144,12 +156,12 @@ else {
public string RoomId { get; set; }
private bool _enableAvatars;
- private StateEventResponse? _currentlyEditingEvent;
- private StateEventResponse? _serverPolicyToMakePermanent;
+ private MatrixEventResponse? _currentlyEditingEvent;
+ private MatrixEventResponse? _serverPolicyToMakePermanent;
- private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
+ private Dictionary<Type, List<MatrixEventResponse>> PolicyEventsByType { get; set; } = new();
- private StateEventResponse? CurrentlyEditingEvent {
+ private MatrixEventResponse? CurrentlyEditingEvent {
get => _currentlyEditingEvent;
set {
_currentlyEditingEvent = value;
@@ -157,7 +169,7 @@ else {
}
}
- private StateEventResponse? ServerPolicyToMakePermanent {
+ private MatrixEventResponse? ServerPolicyToMakePermanent {
get => _serverPolicyToMakePermanent;
set {
_serverPolicyToMakePermanent = value;
@@ -197,12 +209,12 @@ else {
StateHasChanged();
}
- private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
+ private List<MatrixEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
- private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ private List<MatrixEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
.Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
- private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ private List<MatrixEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
.Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
@@ -211,24 +223,24 @@ else {
private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
- private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
+ private async Task RemovePolicyAsync(MatrixEventResponse policyEvent) {
await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { });
PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
await LoadStatesAsync();
}
- private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
+ private async Task UpdatePolicyAsync(MatrixEventResponse policyEvent) {
await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent);
CurrentlyEditingEvent = null;
await LoadStatesAsync();
}
- private async Task UpgradePolicyAsync(StateEventResponse policyEvent) {
+ private async Task UpgradePolicyAsync(MatrixEventResponse policyEvent) {
policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type;
await LoadStatesAsync();
}
- private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+ private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
// event types, unnamed
private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
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..b57beae
--- /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 MatrixEventResponse? 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(MatrixEventResponse 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..3ded78f
--- /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 MatrixEventResponse 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 => MatrixEvent.Equals(x.Value.Policy, evt)).Value;
+ var removals = matchingEntry.DuplicatedBy.RemoveAll(x => MatrixEvent.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 => MatrixEvent.Equals(x.Value.Policy, evt)).Value;
+ var removals = matchingEntry.MadeRedundantBy.RemoveAll(x => MatrixEvent.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(MatrixEventResponse 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
index 2903ab8..a84ef8c 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
@@ -1,12 +1,24 @@
@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>Policy lists </h3> @* <LinkButton href="/Rooms/Create">Create new policy list</LinkButton> *@
+<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>
@@ -16,22 +28,17 @@
}
<hr/>
-<table>
+<table class="table table-striped table-hover table-bordered align-middle" aria-busy="@isLoading">
<thead>
<tr>
- <th/>
<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>
- <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")">
- <span class="oi oi-pencil" aria-hidden="true"></span>
- </LinkButton>
- </td>
<td style="padding-right: 24px;">
<span>@room.RoomName</span>
@if (room.IsLegacy) {
@@ -50,11 +57,44 @@
<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; } = [];
@@ -65,44 +105,39 @@
Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
+ isLoading = true;
Status = "Fetching rooms...";
-
- var userEventTypes = EventContent.GetMatchingEventTypes<UserPolicyRuleEventContent>();
- var serverEventTypes = EventContent.GetMatchingEventTypes<ServerPolicyRuleEventContent>();
- var roomEventTypes = EventContent.GetMatchingEventTypes<RoomPolicyRuleEventContent>();
- var knownPolicyTypes = (List<string>) [..userEventTypes, ..serverEventTypes, ..roomEventTypes];
-
- List<GenericRoom> roomsByType = [];
+ List<Task> _tasks = [];
await foreach (var room in Homeserver.GetJoinedRoomsByType("support.feline.policy.lists.msc.v1")) {
- roomsByType.Add(room);
+ // roomsByType.Add(room);
Status2 = $"Found {room.RoomId} (MSC3784)...";
+ _tasks.Add(Task.Run(async () => {
+ Rooms.Add(await RoomInfo.FromRoom(room));
+ StateHasChanged();
+ }));
}
- List<Task<RoomInfo>> tasks = roomsByType.Select(async room => {
- Status2 = $"Fetching room {room.RoomId}...";
- return await RoomInfo.FromRoom(room);
- }).ToList();
+ await Task.WhenAll(_tasks);
- var results = tasks.ToAsyncEnumerable();
- await foreach (var result in results) {
- Rooms.Add(result);
- StateHasChanged();
- }
+ 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 => knownPolicyTypes.Contains(x.Type))
+ .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);
- })
- .ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var room in rooms) {
if (room is not null) {
@@ -111,31 +146,35 @@
}
}
+ isLoading = false;
Status = "";
Status2 = "";
- await base.OnInitializedAsync();
}
- private string _status;
-
- public string Status {
- get => _status;
+ private string? Status {
+ get;
set {
- _status = value;
+ field = value;
StateHasChanged();
}
}
- private string _status2;
-
- public string Status2 {
- get => _status2;
+ private string? Status2 {
+ get;
set {
- _status2 = value;
+ 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; }
@@ -149,12 +188,7 @@
Server
}
- private static readonly List<string> userEventTypes = EventContent.GetMatchingEventTypes<UserPolicyRuleEventContent>();
- private static readonly List<string> serverEventTypes = EventContent.GetMatchingEventTypes<ServerPolicyRuleEventContent>();
- private static readonly List<string> roomEventTypes = EventContent.GetMatchingEventTypes<RoomPolicyRuleEventContent>();
- private static readonly List<string> allKnownPolicyTypes = [..userEventTypes, ..serverEventTypes, ..roomEventTypes];
-
- public static async Task<RoomInfo> FromRoom(GenericRoom room, List<StateEventResponse>? state = null, bool legacy = false) {
+ public static async Task<RoomInfo> FromRoom(GenericRoom room, List<MatrixEventResponse>? state = null, bool legacy = false) {
state ??= await room.GetFullStateAsListAsync();
return new RoomInfo() {
Room = room,
@@ -165,12 +199,40 @@
?? room.RoomId,
Shortcode = (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode,
PolicyCounts = new() {
- { PolicyType.User, state.Count(x => userEventTypes.Contains(x.Type)) },
- { PolicyType.Server, state.Count(x => serverEventTypes.Contains(x.Type)) },
- { PolicyType.Room, state.Count(x => roomEventTypes.Contains(x.Type)) }
+ { 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/PolicyLists.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css
deleted file mode 100644
index f9b5b3f..0000000
--- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css
+++ /dev/null
@@ -1,6 +0,0 @@
-table, th, td {
- border-width: 1px;
-}
-td {
- padding: 8px;
-}
\ 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..2b1d90a
--- /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<MatrixEvent>>() {
+ { "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<MatrixEvent>(json)); *@
+ @* StateHasChanged(); *@
+ @* })"></FancyTextBox> *@
+ <FancyTextBoxLazyJson T="MatrixEvent" 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<MatrixEvent> events, MatrixEvent 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 46e39ed..93df5a9 100644
--- a/MatrixUtils.Web/Pages/Rooms/Space.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Space.razor
@@ -30,7 +30,7 @@
private GenericRoom? Room { get; set; }
- private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>();
+ private MatrixEventResponse[] States { get; set; } = Array.Empty<MatrixEventResponse>();
private List<RoomInfo> Rooms { get; } = new();
private List<string> ServersInSpace { get; } = new();
private string? NewRoomId { get; set; }
@@ -61,6 +61,7 @@
}
});
}
+
break;
}
case "m.room.member": {
@@ -68,44 +69,46 @@
if (!ServersInSpace.Contains(serverName)) {
ServersInSpace.Add(serverName);
}
+
break;
}
}
}
+
await base.OnInitializedAsync();
- // var state = await Room.GetStateAsync("");
- // if (state is not null) {
- // // Console.WriteLine(state.Value.ToJson());
- // States = state.Value.Deserialize<StateEventResponse[]>()!;
- //
- // foreach (var stateEvent in States) {
- // if (stateEvent.Type == "m.space.child") {
- // // if (stateEvent.Content.ToJson().Length < 5) return;
- // var roomId = stateEvent.StateKey;
- // var room = hs.GetRoom(roomId);
- // if (room is not null) {
- // Rooms.Add(room);
- // }
- // }
- // else if (stateEvent.Type == "m.room.member") {
- // var serverName = stateEvent.StateKey.Split(':').Last();
- // if (!ServersInSpace.Contains(serverName)) {
- // ServersInSpace.Add(serverName);
- // }
- // }
- // }
+ // var state = await Room.GetStateAsync("");
+ // if (state is not null) {
+ // // Console.WriteLine(state.Value.ToJson());
+ // States = state.Value.Deserialize<MatrixEventResponse[]>()!;
+ //
+ // foreach (var stateEvent in States) {
+ // if (stateEvent.Type == "m.space.child") {
+ // // if (stateEvent.Content.ToJson().Length < 5) return;
+ // var roomId = stateEvent.StateKey;
+ // var room = hs.GetRoom(roomId);
+ // if (room is not null) {
+ // Rooms.Add(room);
+ // }
+ // }
+ // else if (stateEvent.Type == "m.room.member") {
+ // var serverName = stateEvent.StateKey.Split(':').Last();
+ // if (!ServersInSpace.Contains(serverName)) {
+ // ServersInSpace.Add(serverName);
+ // }
+ // }
+ // }
- // if(state.Value.TryGetProperty("Type", out var Type))
- // {
- // }
- // else
- // {
- // //this is fine, apprently...
- // //Console.WriteLine($"Room {room.RoomId} has no Content.Type in m.room.create!");
- // }
+ // if(state.Value.TryGetProperty("Type", out var Type))
+ // {
+ // }
+ // else
+ // {
+ // //this is fine, apprently...
+ // //Console.WriteLine($"Room {room.RoomId} has no Content.Type in m.room.create!");
+ // }
- // await base.OnInitializedAsync();
+ // await base.OnInitializedAsync();
}
private async Task JoinAllRooms() {
@@ -120,29 +123,30 @@
var room = Room!.Homeserver.GetRoom(roomId);
if (room is null) return;
try {
- await room.JoinAsync(ServersInSpace.ToArray());
+ await room.JoinAsync(ServersInSpace.Take(10).ToArray());
var joined = false;
while (!joined) {
var ce = await room.GetCreateEventAsync();
- if(ce is null) continue;
+ 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);
- }
+ 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);
+ await Room.AsSpace().AddChildByIdAsync(NewRoomId);
}
}
diff --git a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
index 51cb265..47146bc 100644
--- a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
+++ b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
@@ -37,8 +37,8 @@
[Parameter]
public string? RoomId { get; set; }
- public List<StateEventResponse> FilteredEvents { get; set; } = new();
- public List<StateEventResponse> Events { get; set; } = new();
+ public List<MatrixEventResponse> FilteredEvents { get; set; } = new();
+ public List<MatrixEventResponse> Events { get; set; } = new();
public string status = "";
protected override async Task OnInitializedAsync() {
@@ -58,7 +58,7 @@
var StateLoaded = 0;
var response = (hs.GetRoom(RoomId)).GetFullStateAsync();
await foreach (var _ev in response) {
- // var e = new StateEventResponse {
+ // var e = new MatrixEventResponse {
// Type = _ev.Type,
// StateKey = _ev.StateKey,
// OriginServerTs = _ev.OriginServerTs,
@@ -68,6 +68,7 @@
if (string.IsNullOrEmpty(_ev.StateKey)) {
FilteredEvents.Add(_ev);
}
+
StateLoaded++;
if (!((DateTime.Now - _lastUpdate).TotalMilliseconds > 100)) continue;
@@ -103,11 +104,12 @@
public string content { get; set; }
public long origin_server_ts { get; set; }
public string state_key { get; set; }
+
public string type { get; set; }
- // public string Sender { get; set; }
- // public string EventId { get; set; }
- // public string UserId { get; set; }
- // public string ReplacesState { get; set; }
+ // public string Sender { get; set; }
+ // public string EventId { get; set; }
+ // public string UserId { get; set; }
+ // public string ReplacesState { get; set; }
}
public bool ShowMembershipEvents {
diff --git a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
index c8b87d3..16b1d3d 100644
--- a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
+++ b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
@@ -12,20 +12,20 @@
<table class="table table-striped table-hover" style="width: fit-Content;">
<thead>
- <tr>
- <th scope="col">Type</th>
- <th scope="col">Content</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey == "").OrderBy(x => x.OriginServerTs)) {
<tr>
- <td>@stateEvent.Type</td>
- <td style="max-width: fit-Content;">
- <pre>@stateEvent.RawContent.ToJson()</pre>
- </td>
+ <th scope="col">Type</th>
+ <th scope="col">Content</th>
</tr>
- }
+ </thead>
+ <tbody>
+ @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey == "").OrderBy(x => x.OriginServerTs)) {
+ <tr>
+ <td>@stateEvent.Type</td>
+ <td style="max-width: fit-Content;">
+ <pre>@stateEvent.RawContent.ToJson()</pre>
+ </td>
+ </tr>
+ }
</tbody>
</table>
@@ -34,20 +34,20 @@
<summary>@group.Key</summary>
<table class="table table-striped table-hover" style="width: fit-Content;">
<thead>
- <tr>
- <th scope="col">Type</th>
- <th scope="col">Content</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var stateEvent in group.OrderBy(x => x.OriginServerTs)) {
<tr>
- <td>@stateEvent.Type</td>
- <td style="max-width: fit-Content;">
- <pre>@stateEvent.RawContent.ToJson()</pre>
- </td>
+ <th scope="col">Type</th>
+ <th scope="col">Content</th>
</tr>
- }
+ </thead>
+ <tbody>
+ @foreach (var stateEvent in group.OrderBy(x => x.OriginServerTs)) {
+ <tr>
+ <td>@stateEvent.Type</td>
+ <td style="max-width: fit-Content;">
+ <pre>@stateEvent.RawContent.ToJson()</pre>
+ </td>
+ </tr>
+ }
</tbody>
</table>
</details>
@@ -64,8 +64,8 @@
[Parameter]
public string? RoomId { get; set; }
- public List<StateEventResponse> FilteredEvents { get; set; } = new();
- public List<StateEventResponse> Events { get; set; } = new();
+ public List<MatrixEventResponse> FilteredEvents { get; set; } = new();
+ public List<MatrixEventResponse> Events { get; set; } = new();
public string status = "";
protected override async Task OnInitializedAsync() {
@@ -88,6 +88,7 @@
if (string.IsNullOrEmpty(_ev.StateKey)) {
FilteredEvents.Add(_ev);
}
+
StateLoaded++;
if (!((DateTime.Now - _lastUpdate).TotalMilliseconds > 100)) continue;
diff --git a/MatrixUtils.Web/Pages/Rooms/Timeline.razor b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
index 108581c..f9137b0 100644
--- a/MatrixUtils.Web/Pages/Rooms/Timeline.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
@@ -3,6 +3,7 @@
@using LibMatrix
@using LibMatrix.EventTypes.Spec
@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Responses
<h3>RoomManagerTimeline</h3>
<hr/>
<p>Loaded @Events.Count events...</p>
@@ -21,7 +22,7 @@
public string RoomId { get; set; }
private List<TimelineEventItem> Events { get; } = new();
- private List<StateEventResponse> RawEvents { get; } = new();
+ private List<MatrixEventResponse> RawEvents { get; } = new();
private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
@@ -43,9 +44,9 @@
await base.OnInitializedAsync();
}
- // private StateEventResponse GetProfileEventBefore(StateEventResponse Event) => Events.TakeWhile(x => x != Event).Last(e => e.Type == RoomMemberEventContent.EventId && e.StateKey == Event.Sender);
+ // private MatrixEventResponse GetProfileEventBefore(MatrixEventResponse Event) => Events.TakeWhile(x => x != Event).Last(e => e.Type == RoomMemberEventContent.EventId && e.StateKey == Event.Sender);
- private Type ComponentType(StateEvent Event) => Event.Type switch {
+ private Type ComponentType(MatrixEvent Event) => Event.Type switch {
RoomCanonicalAliasEventContent.EventId => typeof(TimelineCanonicalAliasItem),
RoomHistoryVisibilityEventContent.EventId => typeof(TimelineHistoryVisibilityItem),
RoomTopicEventContent.EventId => typeof(TimelineRoomTopicItem),
@@ -56,9 +57,9 @@
// RoomMessageReactionEventContent.EventId => typeof(ComponentBase),
_ => typeof(TimelineUnknownItem)
};
-
+
private class TimelineEventItem : ComponentBase {
- public StateEventResponse Event { get; set; }
+ public MatrixEventResponse Event { get; set; }
public Type Type { get; set; }
}
diff --git a/MatrixUtils.Web/Pages/ServerInfo.razor b/MatrixUtils.Web/Pages/ServerInfo.razor
index 8dd7907..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) {
diff --git a/MatrixUtils.Web/Pages/StreamTest.razor b/MatrixUtils.Web/Pages/StreamTest.razor
index 8b9735e..949bddc 100644
--- a/MatrixUtils.Web/Pages/StreamTest.razor
+++ b/MatrixUtils.Web/Pages/StreamTest.razor
@@ -48,9 +48,9 @@
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;
+ foreach (var MatrixEventResponse in members) {
+ // Console.WriteLine(MatrixEventResponse.ToJson());
+ var mc = MatrixEventResponse.TypedContent as RoomMemberEventContent;
if (!string.IsNullOrWhiteSpace(mc?.AvatarUrl)) {
var uri = mc.AvatarUrl[6..].Split('/');
var url = $"/_matrix/media/v3/download/{uri[0]}/{uri[1]}";
@@ -89,7 +89,7 @@
// var res2 = Homeserver.ClientHttpClient.GetAsync(url);
// var tasks = Enumerable.Range(1, 128)
// .Select(x => Homeserver.ClientHttpClient.GetStreamAsync(url+$"?width={x*128}&height={x*128}"))
- // .ToAsyncEnumerable();
+ // .ToAsyncResultEnumerable();
await foreach (var result in GetStreamsDelayed(url)) {
Streams.Add(result);
// await Task.Delay(100);
@@ -107,7 +107,7 @@
for (int i = 0; i < 32; i++) {
var tasks = Enumerable.Range(1, 4)
.Select(x => Homeserver.ClientHttpClient.GetStreamAsync(url + $"?width={x * 128}&height={x * 128}&r={Random.Shared.Next(100000)}"))
- .ToAsyncEnumerable();
+ .ToAsyncResultEnumerable();
await foreach (var result in tasks) {
yield return result;
}
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 9a56fc0..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>
diff --git a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
index 0943216..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>
@@ -53,8 +53,8 @@
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);
diff --git a/MatrixUtils.Web/Pages/Tools/Index.razor b/MatrixUtils.Web/Pages/Tools/Index.razor
index f99e932..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/>
@@ -30,6 +31,7 @@
<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 acad827..8ba160a 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
@@ -41,13 +41,13 @@
var ss = new SemaphoreSlim(32, 32);
var rooms = await hs.GetJoinedRooms();
RoomCount = rooms.Count;
- var fetchTasks = rooms.Select(roomId => workerService.TaskPool.Invoke(() => InternalGetMembersByHomeserver(hs.WellKnownUris.Client, hs.AccessToken, roomId.RoomId))).ToList().ToAsyncEnumerable();
+ var fetchTasks = rooms.Select(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;
- // }).ToAsyncEnumerable();
+ // 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)) {
diff --git a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
index f8d1d31..ba8036c 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
@@ -74,7 +74,7 @@ else
}
//use timeline
- var types = StateEventResponse.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent)));
+ var types = MatrixEvent.KnownEventTypes.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) {
@@ -83,7 +83,7 @@ else
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 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();
@@ -100,9 +100,8 @@ else
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)),
@@ -120,10 +119,10 @@ else
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 required MatrixEventResponse Event { get; init; }
+ public required MatrixEventResponse? Previous { get; init; }
- public void Deconstruct(out StateEventTransition transition, out StateEventResponse evt, out StateEventResponse? prev) {
+ public void Deconstruct(out StateEventTransition transition, out MatrixEventResponse evt, out MatrixEventResponse? prev) {
transition = State;
evt = Event;
prev = Previous;
diff --git a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
index ce3513b..76ff629 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
@@ -11,7 +11,8 @@
<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>
@@ -61,7 +60,7 @@
private ObservableCollection<string> log { get; set; } = new();
List<AuthenticatedHomeserverGeneric> hss { get; set; } = new();
ObservableCollection<GenericRoom> rooms { get; set; } = new();
- Dictionary<GenericRoom, FrozenSet<StateEventResponse>> roomMembers { get; set; } = new();
+ Dictionary<GenericRoom, FrozenSet<MatrixEventResponse>> roomMembers { get; set; } = new();
Dictionary<string, List<Matches>> matches = new();
private string UserIdString {
@@ -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);
@@ -148,7 +147,7 @@
private class Matches {
public GenericRoom Room;
- public StateEventResponse Event;
+ public MatrixEventResponse Event;
// public
}
diff --git a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
index 2313884..16a3853 100644
--- a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
+++ b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
@@ -9,7 +9,7 @@
<br/>
<span>Room ID: </span>
<InputText @bind-Value="@roomId"></InputText>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<br/>
diff --git a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
index a252e6b..5b0f510 100644
--- a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
+++ b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
@@ -7,7 +7,7 @@
<br/>
<span>Users:</span>
<InputTextArea @bind-Value="@roomId"></InputTextArea>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<br/>
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
index b0d5a65..1ff97c8 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
@@ -53,7 +53,7 @@
</div>
}
<br/>
-<LinkButton OnClick="@Apply">Apply</LinkButton>
+<LinkButton OnClickAsync="@Apply">Apply</LinkButton>
@code {
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
index ea39c9a..9b0266c 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
@@ -49,7 +49,7 @@
</div>
}
<br/>
-<LinkButton OnClick="@Apply">Apply</LinkButton>
+<LinkButton OnClickAsync="@Apply">Apply</LinkButton>
@code {
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
index 9e70687..69a9048 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
@@ -49,7 +49,7 @@
</div>
}
<br/>
-<LinkButton OnClick="@Apply">Apply</LinkButton>
+<LinkButton OnClickAsync="@Apply">Apply</LinkButton>
@code {
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
index b62cf57..9139561 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
@@ -14,10 +14,10 @@
<p>Users (regex): </p>
<InputTextArea @bind-Value="@UserIdString"></InputTextArea>
-<LinkButton OnClick="Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="Execute">Execute</LinkButton>
<br/>
-<LinkButton OnClick="RemoveKicks">Remove kicks</LinkButton>
-<LinkButton OnClick="RemoveBans">Remove bans</LinkButton>
+<LinkButton OnClickAsync="RemoveKicks">Remove kicks</LinkButton>
+<LinkButton OnClickAsync="RemoveBans">Remove bans</LinkButton>
<br/>
@@ -145,7 +145,7 @@
private class Match {
public GenericRoom Room;
- public StateEventResponse Event;
+ public MatrixEventResponse Event;
public string RoomName { get; set; }
}
@@ -161,7 +161,7 @@
}
return null;
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var result in results) {
if (result is not null) {
yield return result;
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
index 5c5946f..ac68e3d 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
@@ -9,7 +9,7 @@
<br/>
<span>Room ID: </span>
<InputText @bind-Value="@roomId"></InputText>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<br/>
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
index 8fdad84..605890d 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -8,7 +8,7 @@
<br/>
<span>Users:</span>
<InputTextArea @bind-Value="@roomId"></InputTextArea>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<br/>
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index 1ec3cd0..ec1d190 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -16,7 +16,7 @@
<br/>
<span>Room ID: </span>
<InputText @bind-Value="@RoomId"></InputText>
-<LinkButton OnClick="@Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
<p>
<span><InputCheckbox @bind-Value="ChronologicalOrder"/>Chronological order</span>
<span><InputCheckbox @bind-Value="DoDisambiguate"/>Enable extended filters</span>
@@ -30,24 +30,24 @@
<span><InputCheckbox @bind-Value="ShowBans"/> bans</span>
</p>
<p>
- <LinkButton OnClick="@(async () => {
- ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false;
- StateHasChanged();
- })">Hide all
+ <LinkButton OnClickAsync="@(async () => {
+ ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false;
+ StateHasChanged();
+ })">Hide all
</LinkButton>
- <LinkButton OnClick="@(async () => {
- ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true;
- StateHasChanged();
- })">Show all
+ <LinkButton OnClickAsync="@(async () => {
+ ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true;
+ StateHasChanged();
+ })">Show all
</LinkButton>
- <LinkButton OnClick="@(async () => {
- ShowJoins ^= true;
- ShowLeaves ^= true;
- ShowKnocks ^= true;
- ShowInvites ^= true;
- ShowBans ^= true;
- StateHasChanged();
- })">Toggle all
+ <LinkButton OnClickAsync="@(async () => {
+ ShowJoins ^= true;
+ ShowLeaves ^= true;
+ ShowKnocks ^= true;
+ ShowInvites ^= true;
+ ShowBans ^= true;
+ StateHasChanged();
+ })">Toggle all
</LinkButton>
</p>
<p>
@@ -56,25 +56,25 @@
<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>
- }
+ <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>
@@ -129,30 +129,30 @@
</p>
<p>
- <LinkButton OnClick="@(async () => {
- DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false;
- StateHasChanged();
- })">Un-disambiguate all
+ <LinkButton OnClickAsync="@(async () => {
+ DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false;
+ StateHasChanged();
+ })">Un-disambiguate all
</LinkButton>
- <LinkButton OnClick="@(async () => {
- DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true;
- StateHasChanged();
- })">Disambiguate all
+ <LinkButton OnClickAsync="@(async () => {
+ DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true;
+ StateHasChanged();
+ })">Disambiguate all
</LinkButton>
- <LinkButton OnClick="@(async () => {
- DisambiguateProfileUpdates ^= true;
- DisambiguateKicks ^= true;
- DisambiguateUnbans ^= true;
- DisambiguateInviteAccepted ^= true;
- DisambiguateInviteRejected ^= true;
- DisambiguateInviteRetracted ^= true;
- DisambiguateKnockAccepted ^= true;
- DisambiguateKnockRejected ^= true;
- DisambiguateKnockRetracted ^= true;
- DisambiguateKnockActions ^= true;
- DisambiguateInviteActions ^= true;
- StateHasChanged();
- })">Toggle all
+ <LinkButton 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>
}
@@ -306,18 +306,61 @@
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 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 ShowProfileUpdates {
get => field && DisambiguateProfileUpdates;
@@ -399,7 +442,7 @@
#endregion
private ObservableCollection<string> Log { get; set; } = new();
- private List<StateEventResponse> Memberships { get; set; } = [];
+ private List<MatrixEventResponse> Memberships { get; set; } = [];
private AuthenticatedHomeserverGeneric Homeserver { get; set; }
[Parameter, SupplyParameterFromQuery(Name = "room")]
@@ -444,10 +487,10 @@
private readonly struct MembershipEntry {
public required MembershipTransition State { get; init; }
- public required StateEventResponse Event { get; init; }
- public required StateEventResponse? Previous { get; init; }
+ public required MatrixEventResponse Event { get; init; }
+ public required MatrixEventResponse? Previous { get; init; }
- public void Deconstruct(out MembershipTransition transition, out StateEventResponse evt, out StateEventResponse? prev) {
+ public void Deconstruct(out MembershipTransition transition, out MatrixEventResponse evt, out MatrixEventResponse? prev) {
transition = State;
evt = Event;
prev = Previous;
@@ -474,7 +517,7 @@
KnockRetracted
}
- private static IEnumerable<MembershipEntry> GetTransitions(List<StateEventResponse> evts) {
+ private static IEnumerable<MembershipEntry> GetTransitions(List<MatrixEventResponse> 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!");
@@ -528,7 +571,7 @@
{ MembershipTransition.KnockRejected, MembershipTransition.Leave },
{ MembershipTransition.KnockRetracted, MembershipTransition.Leave }
}.ToFrozenDictionary();
-
+
foreach (var entry in entries) {
if (!DoDisambiguate) {
yield return entry;
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
index 736e59a..a8ae603 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
@@ -8,13 +8,13 @@
<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>
@@ -55,7 +55,7 @@
<td>@sets.Item2[0].Room.RoomId</td>
<td>@((sets.Item2[i].Member.TypedContent as RoomMemberEventContent).Membership)</td>
<td>@(roomNames.ContainsKey(sets.Item2[i].Room) ? roomNames[sets.Item2[i].Room] : "")</td>
- <td>@(roomAliasses.ContainsKey(sets.Item2[i].Room) ? roomAliasses[sets.Item2[i].Room] : "")</td>
+ <td>@(roomAliasses.ContainsKey(sets.Item2[i].Room) ? roomAliasses[sets.Item2[i].Room] : "")</td>
}
else {
<td/>
@@ -88,7 +88,7 @@
[Parameter, SupplyParameterFromQuery(Name = "b")]
public string ImportSetBSpaceId { get; set; } = "";
- Dictionary<string, Dictionary<GenericRoom, StateEventResponse>> roomMembers { get; set; } = new();
+ Dictionary<string, Dictionary<GenericRoom, MatrixEventResponse>> roomMembers { get; set; } = new();
Dictionary<string, (List<Match>, List<Match>)> matches { get; set; } = new();
@@ -127,7 +127,7 @@
var setBusers = new Dictionary<string, List<Match>>();
await Task.WhenAll(GetMembers(RoomsA, setAusers), GetMembers(RoomsB, setBusers));
-
+
Log.Add($"Got {setAusers.Count} users in set A");
Log.Add($"Got {setBusers.Count} users in set B");
Log.Add("Calculating intersections...");
@@ -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();
@@ -191,7 +191,7 @@
public class Match {
public GenericRoom Room { get; set; }
- public StateEventResponse Member { get; set; }
+ public MatrixEventResponse Member { get; set; }
}
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
index c3cc09c..d160922 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
@@ -11,7 +11,7 @@
<InputTextArea @bind-Value="@UserIdString"></InputTextArea>
<br/>
<InputText @bind-Value="@ImportFromRoomId"></InputText>
-<LinkButton OnClick="@DoImportFromRoomId">Import from room (ID)</LinkButton>
+<LinkButton OnClickAsync="@DoImportFromRoomId">Import from room (ID)</LinkButton>
<details>
<summary>Rooms to be searched (@rooms.Count)</summary>
@@ -21,15 +21,15 @@
}
</details>
<br/>
-<LinkButton OnClick="Execute">Execute</LinkButton>
+<LinkButton OnClickAsync="Execute">Execute</LinkButton>
<br/>
<details>
<summary>Results</summary>
- @foreach (var (userId, events) in matches.OrderBy(x=>x.Key)) {
+ @foreach (var (userId, events) in matches.OrderBy(x => x.Key)) {
<h4>@userId</h4>
<table>
- @foreach (var match in events.OrderBy(x=>x.RoomName)) {
+ @foreach (var match in events.OrderBy(x => x.RoomName)) {
<tr>
<td>@match.RoomName (<span>@match.Room.RoomId</span>)</td>
<td>
@@ -139,7 +139,7 @@
private class Match {
public GenericRoom Room;
- public StateEventResponse Event;
+ public MatrixEventResponse Event;
public string RoomName { get; set; }
}
@@ -161,7 +161,7 @@
}
return null;
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var result in results) {
if (result is not null) {
yield return result;
@@ -169,7 +169,7 @@
}
}
- public string SummarizeMembership(StateEventResponse state) {
+ public string SummarizeMembership(MatrixEventResponse state) {
var membership = state.ContentAs<RoomMemberEventContent>();
var time = DateTimeOffset.FromUnixTimeMilliseconds(state.OriginServerTs!.Value);
return membership switch {
@@ -186,10 +186,9 @@
_ => $"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 ac3c651..d6ae945 100644
--- a/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
+++ b/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
@@ -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/>
@@ -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 e5ffd5b..acc86a2 100644
--- a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
@@ -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>
@@ -42,7 +42,7 @@
private async Task Execute() {
foreach (var hs in hss) {
var rooms = await hs.GetJoinedRooms();
- var tasks = rooms.Select(x=>ApplyPowerlevelsInRoom(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);
@@ -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 c373a37..ee17f1d 100644
--- a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
@@ -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>
@@ -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/User/Profile.razor b/MatrixUtils.Web/Pages/User/Profile.razor
index ccd3e7b..2b7b6cf 100644
--- a/MatrixUtils.Web/Pages/User/Profile.razor
+++ b/MatrixUtils.Web/Pages/User/Profile.razor
@@ -12,11 +12,15 @@
<div>
<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)) {
@@ -26,7 +30,9 @@
<br/>
@* <details> *@
- <h4>Room profiles<hr></h4>
+ <h4>Room profiles
+ <hr>
+ </h4>
@foreach (var room in Rooms) {
<details class="details-compact">
@@ -41,10 +47,16 @@
@* <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)) {
@@ -117,7 +129,7 @@
});
roomInfoTasks.Add(task);
}
-
+
await Task.WhenAll(roomInfoTasks);
StateHasChanged();
@@ -126,7 +138,7 @@
// 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();
+ // }).ToAsyncResultEnumerable();
// await foreach (var (roomId, roomName) in roomNameTasks) {
// Status = $"Got room name for {roomId}: {roomName}";
diff --git a/MatrixUtils.Web/Program.cs b/MatrixUtils.Web/Program.cs
index 14cf2fc..58b66c1 100644
--- a/MatrixUtils.Web/Program.cs
+++ b/MatrixUtils.Web/Program.cs
@@ -1,8 +1,10 @@
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;
@@ -18,17 +20,16 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
// builder.Logging.SetMinimumLevel(LogLevel.Trace);
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
-builder.Services.AddBlazorJSRuntime();
-builder.Services.AddWebWorkerService(webWorkerService =>
-{
+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 = 2;
+ 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;
+ // webWorkerService.TaskPool.PoolSize = webWorkerService.GlobalScope == GlobalScope.Window ? 0 : 0;
});
try {
@@ -47,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;
@@ -77,7 +77,13 @@ builder.Services.AddScoped<TieredStorageService>(x =>
)
);
+MatrixHttpClient.LogRequests = false;
+
builder.Services.AddRoryLibMatrixServices();
builder.Services.AddScoped<RmuSessionStore>();
+builder.Services.AddSingleton<BlazorSaveFileService>();
+builder.Services.AddSingleton<JsConsoleService>();
+
// await builder.Build().RunAsync();
-await builder.Build().BlazorJSRunAsync();
\ No newline at end of file
+var host = App.Host = builder.Build();
+await host.BlazorJSRunAsync();
\ No newline at end of file
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/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/PolicyEditorComponents/MassPolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
index 11ba18a..b49358d 100644
--- a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
@@ -5,8 +5,9 @@
@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">
+<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>
@@ -14,25 +15,58 @@
<option value="@type">@mappedType.GetFriendlyName().ToLower()</option>
}
</select><br/>
-
+
<span>Reason:</span>
- <FancyTextBox @bind-Value="@Reason"></FancyTextBox><br/>
-
+ <FancyTextBox @bind-Value="@Reason"></FancyTextBox>
+ <br/>
+
<span>Recommendation:</span>
- <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox><br/>
+ <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox>
+ <br/>
<span>Entities:</span><br/>
- <InputTextArea @bind-Value="@Users" style="width: 500px;"></InputTextArea><br/>
-
-
+ <FancyTextBox Multiline="true" @bind-Value="@Entities"></FancyTextBox>
+ <br/>
+
+
@* <details> *@
@* <summary>JSON data</summary> *@
@* <pre> *@
@* $1$ @PolicyEvent.ToJson(true, true) #1# *@
@* </pre> *@
@* </details> *@
- <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton>
- <LinkButton OnClick="@(() => { _ = Save(); return Task.CompletedTask; })"> Save </LinkButton>
+ @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>
@@ -47,33 +81,118 @@
[Parameter]
public required GenericRoom Room { get; set; }
- public string Recommendation { get; set; } = "m.ban";
- public string Reason { get; set; } = "spam";
- public string Users { get; set; } = "";
+ private string Recommendation { get; set; } = "m.ban";
+ private string Reason { get; set; } = "spam";
- private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+ private string Entities { get; set; } = "";
+
+ private string? Response {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private bool VerifyIntent { get; set; }
+
+ private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.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() {
+ 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 DoActualSave();
+ await SaveAll(entities);
}
catch (Exception e) {
- Console.WriteLine($"Failed to save: {e}");
+ Response = $"Failed to save: {e}";
}
}
- private async Task DoActualSave() {
- Console.WriteLine($"Saving ---");
- Console.WriteLine($"Users = {Users}");
- var users = Users.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList();
- var tasks = users.Select(x => ExecuteBan(Room, x)).ToList();
- await Task.WhenAll(tasks);
-
+ 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 MatrixEvent() {
+ Type = MappedType,
+ TypedContent = content,
+ StateKey = content.GetDraupnir2StateKey()
+ };
+ });
+
+ foreach (var chunk in events.Chunk(50))
+ await Room.BulkSendEventsAsync(chunk);
+
OnSaved.Invoke();
}
@@ -81,7 +200,7 @@
bool success = false;
while (!success) {
try {
- var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent;
+ 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;
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 5819bee..501ca99 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">
@@ -52,7 +52,12 @@
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>
+ <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) {
@@ -61,13 +66,22 @@
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>
+ <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>
+ <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>
}
}
@@ -88,8 +102,8 @@
@PolicyEvent.ToJson(true, true)
</pre>
</details>
- <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton>
- <LinkButton OnClick="@(() => { OnSave.Invoke(PolicyEvent); return Task.CompletedTask; })"> Save </LinkButton>
+ <LinkButton OnClickAsync="@InvokeOnClose">Cancel</LinkButton>
+ <LinkButton OnClickAsync="@InvokeOnSave">Save</LinkButton>
}
else {
<p>Policy data is null</p>
@@ -99,7 +113,7 @@
@code {
[Parameter]
- public StateEventResponse? PolicyEvent {
+ public MatrixEventResponse? PolicyEvent {
get => _policyEvent;
set {
if (value is not null && value != _policyEvent)
@@ -111,19 +125,41 @@
}
[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<MatrixEventResponse>? OnSave { get; set; }
+
+ [Parameter]
+ public Func<MatrixEventResponse, 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; }
- private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+ private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.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 StateEventResponse? _policyEvent;
+ private MatrixEventResponse? _policyEvent;
private string? MappedType {
get => _policyEvent?.Type;
diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
index 27f0499..471f586 100644
--- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
+++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
@@ -45,7 +45,7 @@
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 rooms = Space.Room.AsSpace().GetChildrenAsync();
var joinedRooms = await Homeserver.GetJoinedRooms();
await foreach (var room in rooms) {
if (Breadcrumbs.Contains(room.RoomId)) continue;
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
index f107eb3..80c69f2 100644
--- a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
+++ b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
@@ -6,19 +6,19 @@
@code {
[Parameter]
- public StateEventResponse Event { get; set; }
+ public MatrixEventResponse Event { get; set; }
[Parameter]
- public List<StateEventResponse> Events { get; set; }
+ public List<MatrixEventResponse> Events { get; set; }
[Parameter]
public AuthenticatedHomeserverGeneric Homeserver { get; set; }
- public IEnumerable<StateEventResponse> EventsBefore => Events.TakeWhile(e => e.EventId != Event.EventId);
+ public IEnumerable<MatrixEventResponse> EventsBefore => Events.TakeWhile(e => e.EventId != Event.EventId);
- public IEnumerable<StateEventResponse> MatchingEventsBefore => EventsBefore.Where(x => x.Type == Event.Type && x.StateKey == Event.StateKey);
+ public IEnumerable<MatrixEventResponse> MatchingEventsBefore => EventsBefore.Where(x => x.Type == Event.Type && x.StateKey == Event.StateKey);
- public StateEventResponse? PreviousState => MatchingEventsBefore.LastOrDefault();
+ public MatrixEventResponse? PreviousState => MatchingEventsBefore.LastOrDefault();
public RoomMemberEventContent? CurrentSenderMemberEventContent => EventsBefore.LastOrDefault(x => x.Type == "m.room.member" && x.StateKey == Event.Sender)?
.TypedContent as RoomMemberEventContent;
@@ -27,6 +27,4 @@
public bool HasPreviousMessage => EventsBefore.Last() is { Type: "m.room.message" } response && response.Sender == Event.Sender;
-
-
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/UserListItem.razor b/MatrixUtils.Web/Shared/UserListItem.razor
index 5084807..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;">
@@ -26,7 +31,7 @@
[Parameter]
public AuthenticatedHomeserverGeneric _homeserver { get; set; }
- private SvgIdenticonGenerator _identiconGenerator = new();
+ private static SvgIdenticonGenerator _identiconGenerator = new();
protected override async Task OnInitializedAsync() {
// _homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
@@ -37,7 +42,14 @@
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/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 7425de2..fa233b3 100644
--- a/MatrixUtils.Web/wwwroot/index.html
+++ b/MatrixUtils.Web/wwwroot/index.html
@@ -3,42 +3,35 @@
<head>
<meta charset="utf-8"/>
- <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<title>MatrixUtils.Web</title>
<base href="/"/>
- <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet"/>
- <link href="css/app.css" rel="stylesheet"/>
+ <link rel="preload" id="webassembly"/>
+ <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css"/>
+ <link rel="stylesheet" href="css/app.css"/>
+ <link rel="icon" type="image/png" href="favicon.png"/>
+ <link href="MatrixUtils.Web.styles.css" rel="stylesheet"/>
<link rel="manifest" href="rmu.webmanifest"/>
<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="apple-touch-icon" sizes="192x192" href="icon-192.png"/>
+ <script type="importmap"></script>
</head>
<body>
<div id="app">
<svg class="loading-progress">
- <circle cx="50%" cy="50%" r="40%"/>
- <circle cx="50%" cy="50%" r="40%"/>
+ <circle r="40%" cx="50%" cy="50%"/>
+ <circle r="40%" cx="50%" cy="50%"/>
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
- <a class="reload" href="">Reload</a>
- <a class="dismiss">🗙</a>
+ <a href="." class="reload">Reload</a>
+ <span class="dismiss">🗙</span>
</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;
@@ -59,11 +52,11 @@
}
setImageStream = async (element, imageStream) => {
- if(!(element instanceof HTMLElement)) {
+ 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);
@@ -74,8 +67,8 @@
image.src = url;
}
</script>
- <script src="_framework/blazor.webassembly.js"></script>
-<!-- <script>navigator.serviceWorker.register('service-worker.js');</script>-->
+ <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
+ <!-- <script>navigator.serviceWorker.register('service-worker.js');</script>-->
<script src="sw-registrator.js"></script>
</body>
diff --git a/MatrixUtils.Web/wwwroot/service-worker.published.js b/MatrixUtils.Web/wwwroot/service-worker.published.js
index 9219755..3e28e6c 100644
--- a/MatrixUtils.Web/wwwroot/service-worker.published.js
+++ b/MatrixUtils.Web/wwwroot/service-worker.published.js
@@ -9,7 +9,7 @@ self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [// Standard resources
- /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /* Extra known-static paths */
+ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /\.webmanifest$/, /* Extra known-static paths */
/\/_matrix\/media\/.{2}\/download\//, /api\.dicebear\.com\/6\.x\/identicon\/svg/];
const offlineAssetsExclude = [/^service-worker\.js$/];
@@ -22,13 +22,13 @@ async function onInstall(event) {
console.info('Service worker: Install');
// Activate the new service worker as soon as the old one is retired.
- self.skipWaiting();
+ await self.skipWaiting();
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
- .map(asset => new Request(asset.url, {integrity: asset.hash, cache: 'no-cache'}));
+ .map(asset => new Request(asset.url, {cache: 'no-cache'})); /* integrity: asset.hash */
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
@@ -48,7 +48,8 @@ async function onFetch(event) {
// For all navigation requests, try to serve index.html from cache,
// unless that request is for an offline resource.
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
- const shouldServeIndexHtml = event.request.mode === 'navigate' && !manifestUrlList.some(url => url === event.request.url);
+ const shouldServeIndexHtml = event.request.mode === 'navigate'
+ && !manifestUrlList.some(url => url === event.request.url);
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const shouldCache = offlineAssetsInclude.some(pattern => pattern.test(request.url));
@@ -72,7 +73,7 @@ async function onFetch(event) {
fetched, shouldCache, request, exception, cachedResponse, url: request.url,
}
Object.keys(consoleLog).forEach(key => consoleLog[key] == null && delete consoleLog[key])
- if(consoleLog.exception)
+ if (consoleLog.exception)
console.log("Service worker caching: ", consoleLog)
}
diff --git a/MatrixUtils.Web/wwwroot/sw-registrator.js b/MatrixUtils.Web/wwwroot/sw-registrator.js
index 94b96b2..b57d26a 100644
--- a/MatrixUtils.Web/wwwroot/sw-registrator.js
+++ b/MatrixUtils.Web/wwwroot/sw-registrator.js
@@ -8,14 +8,14 @@ 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})`);
// detect updates every minute
setInterval(() => {
registration.update();
- }, 5 * 1000); // 60000ms -> check each minute
+ }, 30 * 1000); // 60000ms -> check each minute
registration.onupdatefound = () => {
const installingServiceWorker = registration.installing;
diff --git a/MatrixUtils.sln b/MatrixUtils.sln
index 5fb0c1f..35840d0 100644
--- a/MatrixUtils.sln
+++ b/MatrixUtils.sln
@@ -1,6 +1,5 @@

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}"
@@ -43,8 +42,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{
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}"
@@ -67,124 +64,399 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions", "MxApiExt
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
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tmp", "tmp\tmp.csproj", "{EC817938-3A2B-44AD-B144-0C439A69433E}"
+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
- {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {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
{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
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|x64.Build.0 = Debug|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|x86.Build.0 = Debug|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|x64.ActiveCfg = Release|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|x64.Build.0 = Release|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|x86.ActiveCfg = Release|Any CPU
+ {EC817938-3A2B-44AD-B144-0C439A69433E}.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}
@@ -202,7 +474,6 @@ Global
{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}
@@ -211,5 +482,7 @@ Global
{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/global.json b/global.json
deleted file mode 100644
index 6d77f62..0000000
--- a/global.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "sdk": {
- "version": "9.0.0",
- "rollForward": "latestMajor",
- "allowPrerelease": true
- }
-}
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index 7c5086f..1f86275 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -12,4 +12,4 @@ rm -rf **/bin/Release
cd MatrixUtils.Web
dotnet publish -c Release
dotnet restore # restore debug deps
-rsync --delete -raP bin/Release/net9.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/
+rsync --delete -raP bin/Release/net10.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/
|