diff --git a/MatrixUtils.Web/Pages/Rooms/Create.razor b/MatrixUtils.Web/Pages/Rooms/Create.razor
index f2dfb01..051d5af 100644
--- a/MatrixUtils.Web/Pages/Rooms/Create.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Create.razor
@@ -3,21 +3,19 @@
@using System.Reflection
@using ArcaneLibs.Extensions
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.Responses
@using MatrixUtils.Web.Classes.RoomCreationTemplates
-@using Microsoft.AspNetCore.Components.Forms
@* @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not */ *@
<h3>Room Manager - Create Room</h3>
@* <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>
@@ -43,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,9 +90,10 @@
<tr>
<td>Room icon:</td>
<td>
- <img src="@Homeserver.ResolveMediaUri(roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/>
+ @* <img src="@Homeserver.ResolveMediaUri(roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/> *@
<div style="display: inline-block; vertical-align: middle;">
- <FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox><br/>
+ <FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox>
+ <br/>
<InputFile OnChange="RoomIconFilePicked"></InputFile>
</div>
</td>
@@ -107,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>
}
@@ -134,7 +144,7 @@
}
else {
<details>
- <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerACLEventContent).Allow.Count) allow rules</summary>
+ <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerAclEventContent).Allow.Count) allow rules</summary>
@* <StringListEditor @bind-Items="@serverAcl.Allow"></StringListEditor> *@
</details>
}
@@ -144,7 +154,7 @@
}
else {
<details>
- <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerACLEventContent).Deny.Count) deny rules</summary>
+ <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerAclEventContent).Deny.Count) deny rules</summary>
@* <StringListEditor @bind-Items="@serverAcl.Allow"></StringListEditor> *@
</details>
}
@@ -256,11 +266,11 @@
private RoomHistoryVisibilityEventContent? historyVisibility => creationEvent?["m.room.history_visibility"].TypedContent as RoomHistoryVisibilityEventContent;
private RoomGuestAccessEventContent? guestAccessEvent => creationEvent?["m.room.guest_access"].TypedContent as RoomGuestAccessEventContent;
- private RoomServerACLEventContent? serverAcl => creationEvent?["m.room.server_acls"].TypedContent as RoomServerACLEventContent;
+ private RoomServerAclEventContent? serverAcl => creationEvent?["m.room.server_acls"].TypedContent as RoomServerAclEventContent;
private RoomAvatarEventContent? roomAvatarEvent => creationEvent?["m.room.avatar"].TypedContent as RoomAvatarEventContent;
protected override async Task OnInitializedAsync() {
- Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) {
@@ -268,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")) {
@@ -301,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 {
@@ -318,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",
@@ -333,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 28c4de2..115c903 100644
--- a/MatrixUtils.Web/Pages/Rooms/Index.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Index.razor
@@ -13,9 +13,7 @@
<p>@Status2</p>
<LinkButton href="/Rooms/Create">Create new room</LinkButton>
-<CascadingValue TValue="AuthenticatedHomeserverGeneric" Value="Homeserver">
- <RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents"></RoomList>
-</CascadingValue>
+<RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents" Homeserver="@Homeserver"></RoomList>
@code {
@@ -68,14 +66,14 @@
// SyncHelper profileSyncHelper;
protected override async Task OnInitializedAsync() {
- Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
// var rooms = await Homeserver.GetJoinedRooms();
// SemaphoreSlim _semaphore = new(160, 160);
GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId);
var filter = await Homeserver.NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetBasicRoomInfo);
- var filterData = await Homeserver.GetFilterAsync(filter);
+ // var filterData = await Homeserver.GetFilterAsync(filter);
// Rooms = new ObservableCollection<RoomInfo>(rooms.Select(room => new RoomInfo(room)));
// foreach (var stateType in filterData.Room?.State?.Types ?? []) {
@@ -99,7 +97,8 @@
syncHelper = new SyncHelper(Homeserver, logger) {
Timeout = 30000,
FilterId = filter,
- MinimumDelay = TimeSpan.FromMilliseconds(5000)
+ MinimumDelay = TimeSpan.FromMilliseconds(5000),
+ UseMsc4222StateAfter = true
};
// profileSyncHelper = new SyncHelper(Homeserver, logger) {
// Timeout = 10000,
@@ -108,9 +107,9 @@
// };
// profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId);
- RunSyncLoop(syncHelper);
+ _ = RunSyncLoop(syncHelper);
// RunSyncLoop(profileSyncHelper);
- RunQueueProcessor();
+ _ = RunQueueProcessor();
await base.OnInitializedAsync();
}
@@ -122,7 +121,7 @@
try {
while (queue.Count == 0) {
Console.WriteLine("Queue is empty, waiting...");
- await Task.Delay(isInitialSync ? 100 : 2500);
+ await Task.Delay(isInitialSync ? 1000 : 2500);
}
Console.WriteLine($"Queue no longer empty after {renderTimeSw.Elapsed}!");
@@ -131,16 +130,16 @@
isInitialSync = false;
while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) {
var (roomId, roomData) = queueEntry;
- Console.WriteLine($"Dequeued room {roomId}");
+ // Console.WriteLine($"Dequeued room {roomId}");
RoomInfo room;
if (Rooms.Any(x => x.Room.RoomId == roomId)) {
room = Rooms.First(x => x.Room.RoomId == roomId);
- Console.WriteLine($"QueueWorker: {roomId} already known with {room.StateEvents?.Count ?? 0} state events");
+ // Console.WriteLine($"QueueWorker: {roomId} already known with {room.StateEvents?.Count ?? 0} state events");
}
else {
- Console.WriteLine($"QueueWorker: encountered new room {roomId}!");
- room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.State?.Events);
+ // Console.WriteLine($"QueueWorker: encountered new room {roomId}!");
+ room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.StateAfter?.Events);
Rooms.Add(room);
}
@@ -149,12 +148,16 @@
throw new InvalidDataException("Somehow this is null???");
}
- if (roomData.State?.Events is { Count: > 0 })
- room.StateEvents.MergeStateEventLists(roomData.State.Events);
- else {
+ if (roomData is { StateAfter.Events.Count: > 0 })
+ room.StateEvents!.MergeStateEventLists(roomData.StateAfter.Events);
+ else
Console.WriteLine($"QueueWorker: could not merge state for {room.Room.RoomId} as new data contains no state events!");
- }
+ if (maxUpdates % 100 == 0) {
+ Console.WriteLine($"QueueWorker: {queue.Count} entries left in queue, {maxUpdates} maxUpdates left, RenderContents: {RenderContents}");
+ StateHasChanged();
+ await Task.Yield();
+ }
// await Task.Delay(100);
}
@@ -170,7 +173,7 @@
}
}
- private bool RenderContents { get; set; } = false;
+ private bool RenderContents { get; set; }
private string _status;
@@ -178,7 +181,8 @@
get => _status;
set {
_status = value;
- StateHasChanged();
+ // StateHasChanged();
+ Console.WriteLine(value);
}
}
@@ -188,7 +192,8 @@
get => _status2;
set {
_status2 = value;
- StateHasChanged();
+ // StateHasChanged();
+ Console.WriteLine(value);
}
}
@@ -200,34 +205,34 @@
var syncs = syncHelper.EnumerateSyncAsync();
await foreach (var sync in syncs) {
- Console.WriteLine("trying sync");
- if (sync is null) continue;
-
var filter = await Homeserver.GetFilterAsync(syncHelper.FilterId);
Status = $"Got sync with {sync.Rooms?.Join?.Count ?? 0} room updates, next batch: {sync.NextBatch}!";
- if (sync?.Rooms?.Join != null)
+ if (sync.Rooms?.Join != null)
foreach (var joinedRoom in sync.Rooms.Join)
- if ( /*joinedRoom.Value.AccountData?.Events?.Count > 0 ||*/ joinedRoom.Value.State?.Events?.Count > 0) {
- joinedRoom.Value.State.Events.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI?.UserId);
+ if (joinedRoom.Value.StateAfter?.Events?.Count > 0) {
+ joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI.UserId);
// We can't trust servers to give us what we ask for, and this ruins performance
// Thanks, Conduit.
- joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false);
- if (filter.Room?.State?.NotSenders?.Any() ?? false)
- joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender) ?? false);
+ if (filter is { Room.State.Types.Count: > 0 })
+ joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false);
+ if (filter is { Room.State.NotSenders.Count: > 0 })
+ joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender!) ?? false);
queue.Enqueue(joinedRoom);
}
- if (sync.Rooms.Leave is { Count: > 0 })
+ if (sync.Rooms?.Leave is { Count: > 0 })
foreach (var leftRoom in sync.Rooms.Leave)
if (Rooms.Any(x => x.Room.RoomId == leftRoom.Key))
Rooms.Remove(Rooms.First(x => x.Room.RoomId == leftRoom.Key));
Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue... " +
- $"{sync?.Rooms?.Join?.Count ?? 0} new updates!";
+ $"{sync.Rooms?.Join?.Count ?? 0} new updates!";
- Status2 = $"Next batch: {sync.NextBatch}";
+ Status2 = $"Next batch: {sync?.NextBatch}";
+ StateHasChanged();
+ await Task.Yield();
}
}
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index b7ebae2..f2ab186 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -1,124 +1,152 @@
@page "/Rooms/{RoomId}/Policies"
@using LibMatrix
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.EventTypes.Spec.State.Policy
@using System.Diagnostics
@using LibMatrix.RoomTypes
@using System.Collections.Frozen
+@using System.Collections.Immutable
@using System.Reflection
+@using System.Text.Json
@using ArcaneLibs.Attributes
+@using ArcaneLibs.Blazor.Components.Services
@using LibMatrix.EventTypes
+@using LibMatrix.EventTypes.Interop.Draupnir
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using SpawnDev.BlazorJS.WebWorkers
+@using MatrixUtils.Web.Pages.Rooms.PolicyListComponents
+@using SpawnDev.BlazorJS
+@inject WebWorkerService WebWorkerService
+@inject ILogger<PolicyList> logger
+@inject BlazorJSRuntime JsRuntime
-@using MatrixUtils.Web.Shared.PolicyEditorComponents
-
-<h3>Policy list editor - Editing @RoomId</h3>
-<hr/>
-@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@
-<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton>
-
-@if (Loading) {
- <p>Loading...</p>
-}
-else if (PolicyEventsByType is not { Count: > 0 }) {
- <p>No policies yet</p>
+@if (!IsInitialised) {
+ <p>Connecting to homeserver...</p>
}
else {
- @foreach (var (type, value) in PolicyEventsByType) {
- <p>
- @(GetValidPolicyEventsByType(type).Count) active,
- @(GetInvalidPolicyEventsByType(type).Count) invalid
- (@value.Count total)
- @(GetPolicyTypeName(type).ToLower())
- </p>
+ <PolicyListEditorHeader Room="@Room" @bind-RenderEventInfo="@RenderEventInfo" ReloadStateAsync="@(() => LoadStateAsync(true))"></PolicyListEditorHeader>
+ @if (Loading) {
+ <p>Loading...</p>
}
+ // else if (PolicyEventsByType is not { Count: > 0 }) {
+ @* <p>No policies yet</p> *@
+ // }
+ else {
+ var renderSw = Stopwatch.StartNew();
+ var renderTotalSw = Stopwatch.StartNew();
+ @foreach (var value in PolicyCollections.Values.OrderByDescending(x => x.TotalCount)) {
+ <p>
+ @value.ActivePolicies.Count active,
+ @value.RemovedPolicies.Count removed
+ (@value.TotalCount total)
+ @value.Name.ToLower()
+ </p>
+ }
- @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) {
- <details>
- <summary>
- <span>
- @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies")
- </span>
- <hr style="margin: revert;"/>
- </summary>
- <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
- @{
- var policies = GetValidPolicyEventsByType(type);
- var invalidPolicies = GetInvalidPolicyEventsByType(type);
- // enumerate all properties with friendly name
- var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
- .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null)
- .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
- .ToFrozenSet();
- var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet();
- }
- <thead>
- <tr>
- @foreach (var name in propNames) {
- <th style="border-width: 1px">@name</th>
- }
- <th style="border-width: 1px">Actions</th>
- </tr>
- </thead>
- <tbody style="border-width: 1px;">
- @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) {
- <tr>
- @{
- var typedContent = policy.TypedContent!;
- var proxySafeProps = typedContent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
- .Where(x => props.Any(y => y.Name == x.Name))
- .ToFrozenSet();
- Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
- }
- @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) {
- <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td>
- }
- <td>
- <div style="display: ruby;">
- @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) {
- <LinkButton OnClick="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton>
- <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Remove</LinkButton>
- @if (policy.IsLegacyType) {
- <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
- }
- }
- </div>
- </td>
- </tr>
- }
- </tbody>
- </table>
- <details>
- <summary>
- <u>
- @("Invalid " + GetPolicyTypeName(type).ToLower())
- </u>
- </summary>
- <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
- <thead>
- <tr>
- <th style="border-width: 1px">State key</th>
- <th style="border-width: 1px">Json contents</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policy in invalidPolicies) {
- <tr>
- <td>@policy.StateKey</td>
- <td>
- <pre>@policy.RawContent.ToJson(true, false)</pre>
- </td>
- </tr>
- }
- </tbody>
- </table>
- </details>
- </details>
- }
-}
+ @if (DuplicateBans?.ActivePolicies.Count > 0) {
+ <p style="color: orange;">
+ Found @DuplicateBans.Value.ActivePolicies.Count duplicate bans
+ </p>
+ }
+
+ @if (RedundantBans?.ActivePolicies.Count > 0) {
+ <p style="color: orange;">
+ Found @RedundantBans.Value.ActivePolicies.Count redundant bans
+ </p>
+ }
+
+ // logger.LogInformation($"Rendered header in {renderSw.GetElapsedAndRestart()}");
+
+ // var renderSw2 = Stopwatch.StartNew();
+ // IOrderedEnumerable<Type> policiesByType = KnownPolicyTypes.Where(t => GetPolicyEventsByType(t).Count > 0).OrderByDescending(t => GetPolicyEventsByType(t).Count);
+ // logger.LogInformation($"Ordered policy types by count in {renderSw2.GetElapsedAndRestart()}");
+
+ @if (DuplicateBans?.ActivePolicies.Count > 0) {
+ <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@DuplicateBans.Value"
+ Room="@Room"></PolicyListCategoryComponent>
+ }
+
+ @if (RedundantBans?.ActivePolicies.Count > 0) {
+ <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@RedundantBans.Value"
+ Room="@Room"></PolicyListCategoryComponent>
+ }
+
+ foreach (var collection in PolicyCollections.Values.OrderByDescending(x => x.ActivePolicies.Count)) {
+ <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent>
+ }
+
+ // foreach (var type in policiesByType) {
+ @* foreach (var type in (List<Type>) []) { *@
+ @* <details> *@
+ @* <summary> *@
+ @* <span> *@
+ @* @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") *@
+ @* </span> *@
+ @* <hr style="margin: revert;"/> *@
+ @* </summary> *@
+ @* <table class="table table-striped table-hover table-bordered align-middle"> *@
+ @* @{ *@
+ @* var renderSw3 = Stopwatch.StartNew(); *@
+ @* var policies = GetValidPolicyEventsByType(type); *@
+ @* var invalidPolicies = GetInvalidPolicyEventsByType(type); *@
+ @* // enumerate all properties with friendly name *@
+ @* var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) *@
+ @* .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) *@
+ @* .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) *@
+ @* .ToFrozenSet(); *@
+ @* var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); *@
+ @* *@
+ @* var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) *@
+ @* .Where(x => props.Any(y => y.Name == x.Name)) *@
+ @* .ToFrozenSet(); *@
+ @* logger.LogInformation($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); *@
+ @* logger.LogInformation($"Filtered policies and got properties in {renderSw3.GetElapsedAndRestart()}"); *@
+ @* } *@
+ @* <thead> *@
+ @* <tr> *@
+ @* @foreach (var name in propNames) { *@
+ @* <th>@name</th> *@
+ @* } *@
+ @* <th>Actions</th> *@
+ @* </tr> *@
+ @* </thead> *@
+ @* <tbody> *@
+ @* @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) { *@
+ @* <PolicyListRowComponent PolicyInfo="@policy" Room="@Room"></PolicyListRowComponent> *@
+ @* } *@
+ @* </tbody> *@
+ @* </table> *@
+ @* <details> *@
+ @* <summary> *@
+ @* <u> *@
+ @* @("Invalid " + GetPolicyTypeName(type).ToLower()) *@
+ @* </u> *@
+ @* </summary> *@
+ @* <table class="table table-striped table-hover"> *@
+ @* <thead> *@
+ @* <tr> *@
+ @* <th>State key</th> *@
+ @* <th>Json contents</th> *@
+ @* </tr> *@
+ @* </thead> *@
+ @* <tbody> *@
+ @* @foreach (var policy in invalidPolicies) { *@
+ @* <tr> *@
+ @* <td>@policy.StateKey</td> *@
+ @* <td> *@
+ @* <pre>@policy.RawContent.ToJson(true, false)</pre> *@
+ @* </td> *@
+ @* </tr> *@
+ @* } *@
+ @* </tbody> *@
+ @* </table> *@
+ @* </details> *@
+ @* </details> *@
+ // }
-@if (CurrentlyEditingEvent is not null) {
- <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal>
+ // logger.LogInformation($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
+ logger.LogInformation("Rendered in {TimeSpan}", renderTotalSw.Elapsed);
+ }
}
@code {
@@ -129,112 +157,472 @@ else {
private const bool Debug = false;
#endif
+ private bool IsInitialised { get; set; } = false;
private bool Loading { get; set; } = true;
- //get room list
- // - sync withroom list filter
- // Type = support.feline.msc3784
- //support.feline.policy.lists.msc.v1
[Parameter]
- public string RoomId { get; set; } = null!;
-
- private bool _enableAvatars;
- private StateEventResponse? _currentlyEditingEvent;
+ public required string RoomId { get; set; }
- // static readonly Dictionary<string, string?> Avatars = new();
- // static readonly Dictionary<string, RemoteHomeserver> Servers = new();
+ [Parameter, SupplyParameterFromQuery]
+ public bool RenderEventInfo {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
- // private static List<StateEventResponse> PolicyEvents { get; set; } = new();
- private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
+ private Dictionary<Type, List<MatrixEventResponse>> PolicyEventsByType { get; set; } = new();
- private StateEventResponse? CurrentlyEditingEvent {
- get => _currentlyEditingEvent;
+ public MatrixEventResponse? ServerPolicyToMakePermanent {
+ get;
set {
- _currentlyEditingEvent = value;
+ field = value;
StateHasChanged();
}
}
- // public bool EnableAvatars {
- // get => _enableAvatars;
- // set {
- // _enableAvatars = value;
- // if (value) GetAllAvatars();
- // }
- // }
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!;
+ private GenericRoom Room { get; set; } = null!;
+ private RoomPowerLevelEventContent PowerLevels { get; set; } = null!;
+ public bool CurrentUserIsDraupnir { get; set; }
+
+ public Dictionary<MatrixEventResponse, int> ActiveKicks { get; set; } = [];
- private AuthenticatedHomeserverGeneric Homeserver { get; set; }
- private GenericRoom Room { get; set; }
- private RoomPowerLevelEventContent PowerLevels { 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();
await base.OnInitializedAsync();
- Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!;
+ Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!;
if (Homeserver is null) return;
Room = Homeserver.GetRoom(RoomId!);
- PowerLevels = (await Room.GetPowerLevelsAsync())!;
- await LoadStatesAsync();
- Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
+ IsInitialised = true;
+ StateHasChanged();
+ await Task.WhenAll(
+ Task.Run(async () => { PowerLevels = (await Room.GetPowerLevelsAsync())!; }),
+ Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>(DraupnirProtectedRoomsData.EventId)) is not null; })
+ );
+ StateHasChanged();
+ await LoadStateAsync(firstLoad: true);
+ Loading = false;
+ logger.LogInformation("Policy list editor initialized in {SwElapsed}!", sw.Elapsed);
}
- private async Task LoadStatesAsync() {
+ private async Task LoadStateAsync(bool firstLoad = false) {
+ // preload workers in task pool
+ // await Task.WhenAll(Enumerable.Range(0, WebWorkerService.MaxWorkerCount).Select(async _ => (await WebWorkerService.TaskPool.GetWorkerAsync()).WhenReady).ToList());
+ var taskPoolReadyTask = WebWorkerService.TaskPool.SetWorkerCount(WebWorkerService.MaxWorkerCount);
+ var sw = Stopwatch.StartNew();
+ // Loading = true;
+ // var states = Room.GetFullStateAsync();
+ var states = await Room.GetFullStateAsListAsync();
+ // PolicyEventsByType.Clear();
+ logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed);
+
+ foreach (var type in KnownPolicyTypes) {
+ if (!PolicyCollections.ContainsKey(type)) {
+ var filterPropSw = Stopwatch.StartNew();
+ // enumerate all properties with friendly name
+ var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null)
+ .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
+ .ToFrozenSet();
+
+ var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => props.Any(y => y.Name == x.Name))
+ .ToFrozenDictionary(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName(), x => x);
+ logger.LogInformation("{Count} proxy safe props found in {TypeFullName} ({TimeSpan})", proxySafeProps?.Count, type.FullName, filterPropSw.Elapsed);
+ PolicyCollections.Add(type, new() {
+ Name = type.GetFriendlyNamePluralOrNull() ?? type.FullName ?? type.Name,
+ ActivePolicies = [],
+ RemovedPolicies = [],
+ PropertiesToDisplay = proxySafeProps
+ });
+ }
+ }
+
+ var count = 0;
+ var parseSw = Stopwatch.StartNew();
+ foreach (var evt in states) {
+ var mappedType = evt.MappedType;
+ if (count % 100 == 0)
+ logger.LogInformation("Processing state #{Count:000000} {EvtType} @ {SwElapsed} (took {ParseSwElapsed:c} so far to process)", count, evt.Type, sw.Elapsed, parseSw.Elapsed);
+ count++;
+
+ if (!mappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
+
+ var collection = PolicyCollections[mappedType];
+
+ var key = (evt.Type, evt.StateKey!);
+ var policyInfo = new PolicyCollection.PolicyInfo {
+ Policy = evt,
+ MadeRedundantBy = [],
+ DuplicatedBy = []
+ };
+ if (evt.RawContent is null or { Count: 0 } || string.IsNullOrWhiteSpace(evt.RawContent?["recommendation"]?.GetValue<string>())) {
+ collection.ActivePolicies.Remove(key);
+ if (!collection.RemovedPolicies.TryAdd(key, policyInfo)) {
+ if (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);
+
+ // return;
+ logger.LogInformation("LoadStatesAsync: Scanning for redundant policies...");
+
+ var scanSw = Stopwatch.StartNew();
+ // var allPolicyInfos = PolicyCollections.Values
+ // .SelectMany(x => x.ActivePolicies.Values)
+ // .ToArray();
+ // var allPolicies = allPolicyInfos
+ // .Select<PolicyCollection.PolicyInfo, (PolicyCollection.PolicyInfo PolicyInfo, PolicyRuleEventContent TypedContent)>(x => (x, (x.Policy.TypedContent as PolicyRuleEventContent)!))
+ // .ToList();
+ // var hashPolicies = allPolicies
+ // .Where(x => x.TypedContent.IsHashedRule())
+ // .ToList();
+ // var wildcardPolicies = allPolicies
+ // .Except(hashPolicies) // hashed policies cannot be wildcards
+ // .Where(x => x.TypedContent.IsGlobRule() || x.TypedContent is ServerPolicyRuleEventContent)
+ // .ToList();
+ // var nonWildcardPolicies = allPolicies
+ // // .Except(wildcardPolicies)
+ // .Where(x => !x.TypedContent!.IsGlobRule() || x.TypedContent is ServerPolicyRuleEventContent)
+ // .ToList();
+ // Console.WriteLine($"Got {allPolicies.Count} total policies, {wildcardPolicies.Count} wildcard policies. Time spent: {scanSw.Elapsed}");
+ // int i = 0;
+ // int hits = 0;
+ // int redundant = 0;
+ // int duplicates = 0;
+
+ // foreach (var (policyInfo, policyContent) in allPolicies) {
+ // foreach (var (otherPolicyInfo, otherPolicyContent) in allPolicies) {
+ // if (policyInfo.Policy == otherPolicyInfo.Policy) continue; // same event
+ // if (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);
+ // }
+ // }
+
+ 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())));
+
+ 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);
+ }
+
+ if (original.MadeRedundantBy.Count > 0) {
+ if (!RedundantBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!)))
+ RedundantBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original);
+ }
+ // Console.WriteLine($"Memory usage: {Util.BytesToString(GC.GetTotalMemory(false))}");
+ }
+
+ Console.WriteLine($"Main: Processed {modifiedPolicyInfos.Count} modified policies in {scanSw.Elapsed} (applied in {applySw.Elapsed})");
+ }
+
+ Console.WriteLine($"Processed {allPolicyInfos.Length} policies in {scanSw.Elapsed}");
+
+ // // scan for wildcard matches
+ // foreach (var policy in allPolicies) {
+ // var matchingPolicies = wildcardPolicies
+ // .Where(x =>
+ // !StateEvent.TypeKeyPairMatches(policy.PolicyInfo.Policy, x.PolicyInfo.Policy)
+ // && x.Item2.EntityMatches(policy.TypedContent.Entity!)
+ // )
+ // .ToList();
+ //
+ // if (matchingPolicies.Count > 0) {
+ // logger.LogInformation($"{i} Got {matchingPolicies.Count} hits for {policy.PolicyInfo.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.PolicyInfo.Policy.RawContent).ToJson()}");
+ // foreach (var match in matchingPolicies) {
+ // policy.PolicyInfo.MadeRedundantBy.Add(match.PolicyInfo.Policy);
+ // }
+ //
+ // hits++;
+ // redundant += matchingPolicies.Count;
+ //
+ // if (hits % 5 == 0)
+ // StateHasChanged();
+ // }
+ // else {
+ // //logger.LogInformation("Sleeping...");
+ // await Task.Delay(1);
+ // }
+ //
+ // i++;
+ // }
+ //
+ // i = 0;
+ // // scan for exact duplicates
+ // foreach (var policy in allPolicies) {
+ // var matchingPolicies = allPolicies
+ // .Where(x =>
+ // !StateEvent.TypeKeyPairMatches(policy.PolicyInfo.Policy, x.PolicyInfo.Policy)
+ // && (
+ // x.Item2.IsHashedRule()
+ // ? x.Item2.EntityMatches(policy.Item2.Entity)
+ // : x.Item2!.Entity == policy.Item2.Entity!
+ // )
+ // )
+ // .ToList();
+ //
+ // if (matchingPolicies.Count > 0) {
+ // logger.LogInformation($"{i} Got {matchingPolicies.Count} duplicates for {policy.PolicyInfo.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.PolicyInfo.Policy.RawContent).ToJson()}");
+ // foreach (var match in matchingPolicies) {
+ // policy.PolicyInfo.MadeRedundantBy.Add(match.PolicyInfo.Policy);
+ // }
+ //
+ // hits++;
+ // duplicates += matchingPolicies.Count;
+ //
+ // if (hits % 5 == 0)
+ // StateHasChanged();
+ // }
+ // else {
+ // //logger.LogInformation("Sleeping...");
+ // await Task.Delay(1);
+ // }
+ //
+ // i++;
+ // }
+ //
+ // logger.LogInformation($"LoadStatesAsync: Found {hits} ({redundant} redundant, {duplicates} duplicates) redundant policies in {sw.Elapsed}");
+ // StateHasChanged();
+ }
+
+ [return: WorkerTransfer]
+ private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(SpawnDev.BlazorJS.JSObjects.String policiesJson, int start, int end) {
+ var policies = JsonSerializer.Deserialize<List<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);
+ }
+
+ [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);
+ }
+
+ [return: WorkerTransfer]
+ private static Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<MatrixEventResponse> policies, int start, int end)
+ => CheckDuplicatePoliciesAsync(policies, start .. end);
+
+ [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>();
+
+ foreach (var (policyEvent, policyContent) in toCheck) {
+ List<MatrixEventResponse> duplicatedBy = [];
+ List<MatrixEventResponse> madeRedundantBy = [];
+
+ 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
+ }
+
+ // if(!policyContent.IsHashedRule())
+ if (!string.IsNullOrWhiteSpace(policyContent.Entity) && policyContent.Entity == otherPolicyContent.Entity) {
+ // Console.WriteLine($"Found duplicate policy: {policyEvent.EventId} is duplicated by {otherPolicyEvent.EventId}");
+ duplicatedBy.Add(otherPolicyEvent);
+ }
+ }
+
+ if (duplicatedBy.Count > 0 || madeRedundantBy.Count > 0) {
+ var summary = $"Policy {policyEvent.EventId} is:";
+ if (duplicatedBy.Count > 0)
+ summary += $"\n- Duplicated by {duplicatedBy.Count} policies: {string.Join(", ", duplicatedBy.Select(x => x.EventId))}";
+ if (madeRedundantBy.Count > 0)
+ summary += $"\n- Made redundant by {madeRedundantBy.Count} policies: {string.Join(", ", madeRedundantBy.Select(x => x.EventId))}";
+ // Console.WriteLine(summary);
+ await jsConsole.Info(summary);
+ await Task.Delay(1);
+ modifiedPolicies.Add(new() {
+ Policy = policyEvent,
+ DuplicatedBy = duplicatedBy,
+ MadeRedundantBy = madeRedundantBy
+ });
+ }
+
+ // await Task.Delay(1);
+ }
+
+ await jsConsole.Info($"Worker: Found {modifiedPolicies.Count} modified policies in range {range} (length: {range.GetOffsetAndLength(policies.Count).Length}) in {sw.Elapsed}");
+
+ return modifiedPolicies;
+ }
+
+ // the old one:
+ private async Task LoadStatesAsync(bool firstLoad = false) {
+ await LoadStateAsync(firstLoad);
+ return;
+ var sw = Stopwatch.StartNew();
Loading = true;
- var states = Room.GetFullStateAsync();
- PolicyEventsByType.Clear();
- await foreach (var state in states) {
- if (state is null) continue;
+ // var states = Room.GetFullStateAsync();
+ var states = await Room.GetFullStateAsListAsync();
+ // PolicyEventsByType.Clear();
+
+ logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed);
+
+ foreach (var type in KnownPolicyTypes) {
+ if (!PolicyEventsByType.ContainsKey(type))
+ PolicyEventsByType.Add(type, new List
+ <MatrixEventResponse>(16000));
+ }
+
+ int count = 0;
+
+ foreach (var state in states) {
+ var _spsw = Stopwatch.StartNew();
+ TimeSpan e1, e2, e3, e4, e5, e6, t;
if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
- if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new());
- PolicyEventsByType[state.MappedType].Add(state);
+ e1 = _spsw.Elapsed;
+ var targetPolicies = PolicyEventsByType[state.MappedType];
+ e2 = _spsw.Elapsed;
+ if (!firstLoad && targetPolicies.FirstOrDefault(x => 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();
+ }
+
+ e4 = _spsw.Elapsed;
+ logger.LogInformation("[E] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={E3:c}, e4={E4:c}, e5={Zero:c},t={SpswElapsed:c})", count++, state.Type, sw.Elapsed, e1, e2, e3, e4, TimeSpan.Zero, _spsw.Elapsed);
+ continue;
+ }
+
+ e4 = _spsw.Elapsed;
+ targetPolicies.Remove(evt);
+ e5 = _spsw.Elapsed;
+ targetPolicies.Add(state);
+ e6 = _spsw.Elapsed;
+ t = _spsw.Elapsed;
+ logger.LogInformation("[M] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={E3:c}, e4={E4:c}, e5={E5:c}, e6={E6:c},t={TimeSpan1:c})", count++, state.Type, sw.Elapsed, e1, e2, e3, e4, e5, e6, t);
+ }
+ else {
+ targetPolicies.Add(state);
+ t = _spsw.Elapsed;
+ logger.LogInformation("[N] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={Zero:c}, e4={TimeSpan1:c}, e5={Zero1:c}, e6={TimeSpan2:c}, t={TimeSpan3:c})", count++, state.Type, sw.Elapsed, e1, e2, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, t);
+ }
+
+ // await Task.Delay(10);
+ // await Task.Yield();
}
+ logger.LogInformation("LoadStatesAsync: Processed state in {SwElapsed}", sw.Elapsed);
+
Loading = false;
StateHasChanged();
+ await Task.Delay(10);
+ await Task.Yield();
+ logger.LogInformation("LoadStatesAsync: yield finished in {SwElapsed}", sw.Elapsed);
}
- // private async Task GetAllAvatars() {
- // // if (!_enableAvatars) return;
- // Console.WriteLine("Getting avatars...");
- // var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue<string>()).Where(x => x.Contains(':') && !x.Contains("*")).ToList();
- // Console.WriteLine($"Got {users.Count} users!");
- // var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList());
- // Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!");
- // var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable();
- // await foreach (var server in homeserverTasks) {
- // if (server is null) continue;
- // var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList();
- // await Task.WhenAll(profileTasks);
- // profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } });
- // foreach (var profile in profileTasks.Select(x => x.Result!.Value)) {
- // // if (profile is null) continue;
- // if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) {
- // var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl);
- // Avatars.TryAdd(profile.Key, url);
- // }
- // else Avatars.TryAdd(profile.Key, null);
- // }
+ 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();
//
- // StateHasChanged();
- // }
- // }
+ // private List<MatrixEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ // .Where(x => x.RawContent is { Count: > 0 } && string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
//
- // private async Task<KeyValuePair<string, UserProfileResponse>?> TryGetProfile(RemoteHomeserver server, string mxid) {
- // try {
- // return new KeyValuePair<string, UserProfileResponse>(mxid, await server.GetProfileAsync(mxid));
- // }
- // catch {
- // return null;
- // }
- // }
-
- private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
-
- private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
- .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
-
- private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
- .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+ // private List<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>()
@@ -242,27 +630,34 @@ else {
private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
- private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
- await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { });
- PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
- await LoadStatesAsync();
- }
+ public struct PolicyCollection {
+ public required string Name { get; init; }
+ public SpecialViewType ViewType { get; init; }
+ public int TotalCount => ActivePolicies.Count + RemovedPolicies.Count;
- private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
- await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent);
- CurrentlyEditingEvent = null;
- await LoadStatesAsync();
- }
+ public required Dictionary<(string Type, string StateKey), PolicyInfo> ActivePolicies { get; set; }
- private async Task UpgradePolicyAsync(StateEventResponse policyEvent) {
- policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type;
- await LoadStatesAsync();
- }
+ // public Dictionary<(string Type, string StateKey), MatrixEventResponse> InvalidPolicies { get; set; }
+ public required Dictionary<(string Type, string StateKey), PolicyInfo> RemovedPolicies { get; set; }
+ public required FrozenDictionary<string, PropertyInfo> PropertiesToDisplay { get; set; }
- private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+ public class PolicyInfo {
+ public required MatrixEventResponse Policy { get; init; }
+ public required List<MatrixEventResponse> MadeRedundantBy { get; set; }
+ public required List<MatrixEventResponse> DuplicatedBy { get; set; }
+ }
- // event types, unnamed
- private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
- .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+ public enum SpecialViewType {
+ None,
+ Duplicates,
+ Redundant,
+ }
+ }
+
+ // private struct PolicyStats {
+ // public int Active { get; set; }
+ // public int Invalid { get; set; }
+ // public int Removed { get; set; }
+ // }
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs
new file mode 100644
index 0000000..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/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
new file mode 100644
index 0000000..ac918a8
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
@@ -0,0 +1,252 @@
+@page "/Rooms/{RoomId}/Policies2"
+@using LibMatrix
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using System.Diagnostics
+@using LibMatrix.RoomTypes
+@using System.Collections.Frozen
+@using System.Reflection
+@using ArcaneLibs.Attributes
+@using LibMatrix.EventTypes
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+
+@using MatrixUtils.Web.Shared.PolicyEditorComponents
+
+<h3>Policy list editor - Editing @RoomId</h3>
+<hr/>
+@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@
+<LinkButton OnClickAsync="@(() => {
+ CurrentlyEditingEvent = new() { Type = "", RawContent = new() };
+ return Task.CompletedTask;
+ })">Create new policy
+</LinkButton>
+
+@if (Loading) {
+ <p>Loading...</p>
+}
+else if (PolicyEventsByType is not { Count: > 0 }) {
+ <p>No policies yet</p>
+}
+else {
+ var renderSw = Stopwatch.StartNew();
+ var renderTotalSw = Stopwatch.StartNew();
+ @foreach (var (type, value) in PolicyEventsByType) {
+ <p>
+ @(GetValidPolicyEventsByType(type).Count) active,
+ @(GetInvalidPolicyEventsByType(type).Count) invalid
+ (@value.Count total)
+ @(GetPolicyTypeName(type).ToLower())
+ </p>
+ }
+
+ Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}");
+
+ @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) {
+ <details>
+ <summary>
+ <span>
+ @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies")
+ </span>
+ <hr style="margin: revert;"/>
+ </summary>
+ <div class="flex-grid">
+ @{
+ var policies = GetValidPolicyEventsByType(type);
+ var invalidPolicies = GetInvalidPolicyEventsByType(type);
+ // enumerate all properties with friendly name
+ var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null)
+ .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
+ .ToFrozenSet();
+ var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet();
+
+ var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => props.Any(y => y.Name == x.Name))
+ .ToFrozenSet();
+ Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
+ }
+ @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) {
+ <div class="flex-item">
+ @{
+ var typedContent = policy.TypedContent!;
+ }
+ @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) {
+ <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td>
+ }
+ <div style="display: ruby;">
+ @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) {
+ <LinkButton OnClickAsync="@(() => {
+ CurrentlyEditingEvent = policy;
+ return Task.CompletedTask;
+ })">Edit
+ </LinkButton>
+ <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Remove</LinkButton>
+ @if (policy.IsLegacyType) {
+ <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
+ }
+
+ @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.EventId)) {
+ <LinkButton OnClickAsync="@(() => {
+ ServerPolicyToMakePermanent = policy;
+ return Task.CompletedTask;
+ })">Make permanent (wildcard)
+ </LinkButton>
+ @if (CurrentUserIsDraupnir) {
+ <LinkButton OnClickAsync="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton>
+ }
+ }
+ else {
+ <p>meow</p>
+ }
+ }
+ else {
+ <p>No permission to modify</p>
+ }
+ </div>
+ </div>
+ }
+ </div>
+ <details>
+ <summary>
+ <u>
+ @("Invalid " + GetPolicyTypeName(type).ToLower())
+ </u>
+ </summary>
+ <table class="table table-striped table-hover">
+ <thead>
+ <tr>
+ <th>State key</th>
+ <th>Json contents</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var policy in invalidPolicies) {
+ <tr>
+ <td>@policy.StateKey</td>
+ <td>
+ <pre>@policy.RawContent.ToJson(true, false)</pre>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+ </details>
+ }
+
+ Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
+ Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}");
+}
+
+@if (CurrentlyEditingEvent is not null) {
+ <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal>
+}
+
+@code {
+
+#if DEBUG
+ private const bool Debug = true;
+#else
+ private const bool Debug = false;
+#endif
+
+ private bool Loading { get; set; } = true;
+
+ [Parameter]
+ public string RoomId { get; set; }
+
+ private bool _enableAvatars;
+ private MatrixEventResponse? _currentlyEditingEvent;
+ private MatrixEventResponse? _serverPolicyToMakePermanent;
+
+ private Dictionary<Type, List<MatrixEventResponse>> PolicyEventsByType { get; set; } = new();
+
+ private MatrixEventResponse? CurrentlyEditingEvent {
+ get => _currentlyEditingEvent;
+ set {
+ _currentlyEditingEvent = value;
+ StateHasChanged();
+ }
+ }
+
+ private MatrixEventResponse? ServerPolicyToMakePermanent {
+ get => _serverPolicyToMakePermanent;
+ set {
+ _serverPolicyToMakePermanent = value;
+ StateHasChanged();
+ }
+ }
+
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; }
+ private GenericRoom Room { get; set; }
+ private RoomPowerLevelEventContent PowerLevels { get; set; }
+ private bool CurrentUserIsDraupnir { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var sw = Stopwatch.StartNew();
+ await base.OnInitializedAsync();
+ Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!;
+ if (Homeserver is null) return;
+ Room = Homeserver.GetRoom(RoomId!);
+ PowerLevels = (await Room.GetPowerLevelsAsync())!;
+ CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>("org.matrix.mjolnir.protected_rooms")) is not null;
+ await LoadStatesAsync();
+ Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
+ }
+
+ private async Task LoadStatesAsync() {
+ Loading = true;
+ var states = Room.GetFullStateAsync();
+ PolicyEventsByType.Clear();
+ await foreach (var state in states) {
+ if (state is null) continue;
+ if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
+ if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new());
+ PolicyEventsByType[state.MappedType].Add(state);
+ }
+
+ Loading = false;
+ StateHasChanged();
+ }
+
+ private List<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 => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+
+ private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
+ ?? type.GetCustomAttributes<MatrixEventAttribute>()
+ .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName;
+
+ private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
+
+ private async Task RemovePolicyAsync(MatrixEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { });
+ PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
+ await LoadStatesAsync();
+ }
+
+ private async Task UpdatePolicyAsync(MatrixEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent);
+ CurrentlyEditingEvent = null;
+ await LoadStatesAsync();
+ }
+
+ private async Task UpgradePolicyAsync(MatrixEventResponse policyEvent) {
+ policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type;
+ await LoadStatesAsync();
+ }
+
+ 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());
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css
new file mode 100644
index 0000000..d224737
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css
@@ -0,0 +1,32 @@
+th {
+ border-width: 1px;
+}
+
+table {
+ width: fit-content;
+ border-width: 1px;
+ vertical-align: middle;
+}
+
+.flex-grid {
+ display: grid;
+ /*grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));*/
+ /*// fit based on content max width*/
+ grid-template-columns: repeat(auto-fill, minmax(min-content, 1fr));
+
+ gap: 10px;
+}
+
+.flex-item {
+ /*flex: 1 1 30%;*/
+ /*margin: 0.25rem;*/
+ /*position: relative;*/
+ /*display: flex;*/
+ /*flex-direction: column;*/
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #fff1;
+ background-clip: border-box;
+ border: 1px solid rgba(0, 0, 0, .125);
+ border-radius: .5rem
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
new file mode 100644
index 0000000..932e0fe
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
@@ -0,0 +1,74 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.RoomTypes
+<details>
+ <summary>
+ <span>
+ @($"{PolicyCollection.Name}: {PolicyCollection.TotalCount} policies")
+ </span>
+ <hr style="margin: revert;"/>
+ </summary>
+ <table class="table table-striped table-hover table-bordered align-middle">
+ <thead>
+ <tr>
+ <th>Actions</th>
+ @foreach (var name in PolicyCollection.PropertiesToDisplay!.Keys) {
+ <th>@name</th>
+ }
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var policy in PolicyCollection.ActivePolicies.Values.OrderBy(x => x.Policy.RawContent?["entity"]?.GetValue<string>())) {
+ <PolicyListRowComponent PolicyCollectionStateHasChanged="@StateHasChanged" RenderEventInfo="RenderEventInfo" PolicyInfo="@policy" PolicyCollection="@PolicyCollection" Room="@Room"></PolicyListRowComponent>
+ }
+ </tbody>
+ </table>
+ @if (RenderInvalidSection) {
+ <details>
+ <summary>
+ <u>
+ @("Invalid " + PolicyCollection.Name.ToLower())
+ </u>
+ </summary>
+ <table class="table table-striped table-hover table-bordered align-middle">
+ <thead>
+ <tr>
+ <th>State key</th>
+ <th>Json contents</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var policy in PolicyCollection.RemovedPolicies.Values) {
+ <tr>
+ <td>@policy.Policy.StateKey</td>
+ <td>
+ <pre>@policy.Policy.RawContent.ToJson(true, false)</pre>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+ }
+</details>
+
+@code {
+
+ [Parameter]
+ public required PolicyList.PolicyCollection PolicyCollection { get; set; }
+
+ [Parameter]
+ public required GenericRoom Room { get; set; }
+
+ [Parameter]
+ public bool RenderEventInfo { get; set; }
+
+ [Parameter]
+ public bool RenderInvalidSection { get; set; } = true;
+
+ protected override bool ShouldRender() {
+ // if (PolicyCollection is null) return false;
+
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
new file mode 100644
index 0000000..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
new file mode 100644
index 0000000..a84ef8c
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
@@ -0,0 +1,238 @@
+@page "/PolicyLists"
+@using ArcaneLibs
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes
+@using LibMatrix.EventTypes.Common
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using LibMatrix.Helpers
+@using LibMatrix.Responses
+@using LibMatrix.RoomTypes
+@inject ILogger<Index> logger
+<h3>
+ <span>Policy lists </span>
+ <LinkButton OnClickAsync="@(() => {
+ ShowPolicyListCreationWindow = true;
+ return Task.CompletedTask;
+ })">
+ <span class="oi oi-plus" aria-hidden="true"> Create</span>
+ </LinkButton>
+</h3>
+
+
+@if (!string.IsNullOrWhiteSpace(Status)) {
+ <p>@Status</p>
+}
+@if (!string.IsNullOrWhiteSpace(Status2)) {
+ <p>@Status2</p>
+}
+<hr/>
+
+<table class="table table-striped table-hover table-bordered align-middle" aria-busy="@isLoading">
+ <thead>
+ <tr>
+ <th>Room name</th>
+ <th>Policies</th>
+ <th/>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var room in Rooms.OrderByDescending(x => x.PolicyCounts.Sum(y => y.Value))) {
+ <tr>
+ <td style="padding-right: 24px;">
+ <span>@room.RoomName</span>
+ @if (room.IsLegacy) {
+ <span style="color: red;"> (legacy)</span>
+ }
+ <br/>
+ @if (!string.IsNullOrWhiteSpace(room.Shortcode)) {
+ <span style="font-size: 0.8em;">@room.Shortcode</span>
+ }
+ else {
+ <span style="color: red;">(no shortcode)</span>
+ }
+ </td>
+ <td>
+ <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.User) ?? 0) user policies</span><br/>
+ <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Server) ?? 0) server policies</span><br/>
+ <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Room) ?? 0) room policies</span><br/>
+ </td>
+ <td>
+ <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")">
+ <span class="oi oi-pencil" aria-hidden="true"> View/edit policies</span>
+ </LinkButton>
+ </td>
+ </tr>
+ }
+ </tbody>
+</table>
+
+@if (ShowPolicyListCreationWindow && Homeserver != null) {
+ <ModalWindow Title="New policy list">
+ @if (!string.IsNullOrWhiteSpace(_roomBuilder.Avatar.Url)) {
+ <MxcAvatar Homeserver="@Homeserver" MxcUri="@_roomBuilder.Avatar.Url" Circular="true" Size="4" SizeUnit="em"/>
+ }
+ else {
+ <img class="avatar" style="height: 4em; width: 4em; border-radius: 50%;" src="@IdenticonGenerator.GenerateAsDataUri(Homeserver.WhoAmI.UserId)"/>
+ }
+ <div style="display: inline-block; vertical-align: middle; padding-left: 1em;">
+ <FancyTextBox @bind-Value="@_roomBuilder.Name.Name"></FancyTextBox>
+ <br/>
+ <span>#</span>
+ <FancyTextBox @bind-Value="@_roomBuilder.AliasLocalPart"></FancyTextBox>
+ <span>:@Homeserver!.ServerName</span>
+ <br/>
+ <FancyTextBox @bind-Value="@_roomBuilder.Avatar.Url"></FancyTextBox>
+ <InputFile OnChange="@RoomIconFilePicked"></InputFile>
+ </div>
+ <br/>
+
+ <span>Bot shortcode: </span>
+ <FancyTextBox @bind-Value="@_shortcodeEvent.Shortcode"></FancyTextBox>
+ <br/>
+ <LinkButton OnClickAsync="@CreatePolicyList">Create</LinkButton>
+
+ </ModalWindow>
+}
+
+@code {
+
+ private List<RoomInfo> Rooms { get; } = [];
+
+ private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (Homeserver is null) return;
+
+ isLoading = true;
+ Status = "Fetching rooms...";
+ List<Task> _tasks = [];
+ await foreach (var room in Homeserver.GetJoinedRoomsByType("support.feline.policy.lists.msc.v1")) {
+ // roomsByType.Add(room);
+ Status2 = $"Found {room.RoomId} (MSC3784)...";
+ _tasks.Add(Task.Run(async () => {
+ Rooms.Add(await RoomInfo.FromRoom(room));
+ StateHasChanged();
+ }));
+ }
+
+ await Task.WhenAll(_tasks);
+
+ isLoading = false;
+ Status = "";
+ Status2 = "";
+ }
+
+ private async Task ScanLegacyLists() {
+ isLoading = true;
+ Status = "Searching for legacy lists...";
+ var rooms = (await Homeserver.GetJoinedRooms())
+ .Where(x => !Rooms.Any(y => y.Room.RoomId == x.RoomId))
+ .Select(async room => {
+ var state = await room.GetFullStateAsListAsync();
+ var policies = state
+ .Where(x => PolicyRoom.SpecPolicyEventTypes.Contains(x.Type))
+ .ToList();
+ if (policies.Count == 0) return null;
+ Status2 = $"Found legacy list {room.RoomId}...";
+ return await RoomInfo.FromRoom(room, state, true);
+ }).ToAsyncResultEnumerable();
+
+ await foreach (var room in rooms) {
+ if (room is not null) {
+ Rooms.Add(room);
+ StateHasChanged();
+ }
+ }
+
+ isLoading = false;
+ Status = "";
+ Status2 = "";
+ }
+
+ private string? Status {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private string? Status2 {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private bool ShowPolicyListCreationWindow {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ } = true;
+
+ private class RoomInfo {
+ public GenericRoom Room { get; set; }
+ public string RoomName { get; set; }
+ public string? Shortcode { get; set; }
+ public Dictionary<PolicyType, int?> PolicyCounts { get; set; }
+ public bool IsLegacy { get; set; }
+
+ public enum PolicyType {
+ User,
+ Room,
+ Server
+ }
+
+ public static async Task<RoomInfo> FromRoom(GenericRoom room, List<MatrixEventResponse>? state = null, bool legacy = false) {
+ state ??= await room.GetFullStateAsListAsync();
+ return new RoomInfo() {
+ Room = room,
+ IsLegacy = legacy,
+ RoomName = await room.GetNameAsync()
+ ?? (await room.GetCanonicalAliasAsync())?.Alias
+ ?? (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode
+ ?? room.RoomId,
+ Shortcode = (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode,
+ PolicyCounts = new() {
+ { PolicyType.User, state.Count(x => PolicyRoom.UserPolicyEventTypes.Contains(x.Type)) },
+ { PolicyType.Server, state.Count(x => PolicyRoom.ServerPolicyEventTypes.Contains(x.Type)) },
+ { PolicyType.Room, state.Count(x => PolicyRoom.RoomPolicyEventTypes.Contains(x.Type)) }
+ }
+ };
+ }
+ }
+
+ private readonly RoomBuilder _roomBuilder = new() {
+ Type = "support.feline.policy.lists.msc.v1",
+ Name = new() { Name = "New policy list" },
+ AliasLocalPart = "policies"
+ };
+
+ private readonly MjolnirShortcodeEventContent _shortcodeEvent = new() {
+ Shortcode = "policy-list"
+ };
+
+ private bool isLoading = true;
+
+ private static readonly SvgIdenticonGenerator IdenticonGenerator = new();
+
+ private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) {
+ var res = await Homeserver!.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType);
+ Console.WriteLine(res);
+ _roomBuilder.Avatar.Url = res;
+ StateHasChanged();
+ }
+
+ private async Task CreatePolicyList() {
+ var room = await _roomBuilder.Create(Homeserver!);
+ Status = $"Created policy list {room.RoomId} ({room.GetNameAsync()})";
+ await room.SendStateEventAsync(MjolnirShortcodeEventContent.EventId, _shortcodeEvent);
+ NavigationManager.NavigateTo($"/Rooms/{room.RoomId}/Policies");
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor
new file mode 100644
index 0000000..c1ee202
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor
@@ -0,0 +1,52 @@
+@using ArcaneLibs
+@using LibMatrix.Helpers
+<tr>
+ <td>Room name:</td>
+ <td>
+ <FancyTextBox @bind-Value="@roomBuilder.Name.Name"></FancyTextBox>
+ </td>
+</tr>
+<tr>
+ <td>Room alias:</td>
+ <td>
+ <InputLocalPart Sigil="#" ServerName="@Homeserver.ServerName" @bind-LocalPart="@roomBuilder.AliasLocalPart"></InputLocalPart>
+ </td>
+</tr>
+<tr>
+ <td>Room icon:</td>
+ <td>
+ @if (!string.IsNullOrWhiteSpace(roomBuilder.Avatar.Url)) {
+ <MxcAvatar Homeserver="Homeserver" MxcUri="@roomBuilder.Avatar.Url" Size="3" SizeUnit="em" Circular="true"/>
+ }
+ else {
+ <img class="avatar" style="height: 3em; width: 3em; border-radius: 50%;" src="@IdenticonGenerator.GenerateAsDataUri(Homeserver.WhoAmI.UserId)"/>
+ }
+ <div style="display: inline-block; vertical-align: middle;">
+ <FancyTextBox @bind-Value="@roomBuilder.Avatar.Url"></FancyTextBox>
+ <br/>
+ <SimpleFilePicker OnFilePicked="@RoomIconFilePicked"></SimpleFilePicker>
+ </div>
+ </td>
+</tr>
+
+@code {
+
+ [Parameter]
+ public required RoomBuilder roomBuilder { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+ private static readonly SvgIdenticonGenerator IdenticonGenerator = new();
+
+ private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) {
+ var res = await Homeserver.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType);
+ Console.WriteLine(res);
+ roomBuilder.Avatar.Url = res;
+ PageStateHasChanged();
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor
new file mode 100644
index 0000000..3f4a73d
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor
@@ -0,0 +1,92 @@
+@using Blazored.LocalStorage
+@using LibMatrix.Helpers
+@inject ILocalStorageService LocalStorage
+<tr>
+ <td>Room type:</td>
+ <td>
+ @if (RoomTypes.ContainsKey(roomBuilder.Type ?? "")) {
+ <InputSelect @bind-Value="@roomBuilder.Type">
+ @foreach (var type in RoomTypes) {
+ <option value="@type.Key">@type.Value</option>
+ }
+ <option value="custom">Custom ...</option>
+ </InputSelect>
+ }
+ else {
+ <FancyTextBox @bind-Value="@roomBuilder.Type"></FancyTextBox>
+ }
+
+ <span> version </span>
+ @if (Capabilities is null) {
+ <span style="color: #888;">Loading...</span>
+ }
+ else {
+ <InputSelect @bind-Value="@roomBuilder.Version">
+ @foreach (var version in Capabilities.Capabilities.RoomVersions!.Available!) {
+ <option value="@version.Key">@version.Key (@version.Value)</option>
+ }
+ </InputSelect>
+ }
+ </td>
+</tr>
+<tr>
+ <td style="vertical-align: top;">Allow attribution:</td>
+ <td>
+ <InputCheckbox @bind-Value="@AllowAttribution"/>
+ <span>Allow attribution to Rory&::MatrixUtils</span>
+ <LinkButton InlineText="true" OnClick="@(() => ShowAttributionInfo = true)">?</LinkButton>
+ </td>
+</tr>
+
+@if (ShowAttributionInfo) {
+ <ModalWindow Title="Allow attribution to Rory&::MatrixUtils"
+ OnCloseClicked="@(() => ShowAttributionInfo = false)">
+ <span>This will add the following to the room creation content:</span>
+ <br/>
+ <pre>{ "gay.rory.created_using": "Rory&::MatrixUtils (https://mru.rory.gay)" }</pre>
+ <span>This is not visible to users unless they manually inspect the room's create event source.</span>
+ </ModalWindow>
+}
+
+@code {
+
+ [Parameter]
+ public required RoomBuilder roomBuilder { get; set; }
+
+ [Parameter]
+ public required Action PageStateHasChanged { get; set; }
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+ private AuthenticatedHomeserverGeneric.CapabilitiesResponse? Capabilities { get; set; }
+
+ private bool ShowAttributionInfo {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private bool AllowAttribution {
+ get;
+ set {
+ field = value;
+ _ = LocalStorage.SetItemAsync("rmu.room_create.allow_attribution", value);
+ }
+ } = true;
+
+ protected override async Task OnInitializedAsync() {
+ Capabilities = await Homeserver.GetCapabilitiesAsync();
+ roomBuilder.Version = Capabilities.Capabilities.RoomVersions!.Default;
+ AllowAttribution = await LocalStorage.GetItemAsync<bool?>("rmu.room_create.allow_attribution") ?? true;
+ }
+
+ private static Dictionary<string, string> RoomTypes { get; } = new() {
+ { "", "Room" },
+ { "m.space", "Space" },
+ { "support.feline.policy.lists.msc.v1", "Policy list" }
+ };
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor
new file mode 100644
index 0000000..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 01ab1c4..93df5a9 100644
--- a/MatrixUtils.Web/Pages/Rooms/Space.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Space.razor
@@ -2,11 +2,15 @@
@using LibMatrix.RoomTypes
@using ArcaneLibs.Extensions
@using LibMatrix
+@using MatrixUtils.Abstractions
<h3>Room manager - Viewing Space</h3>
+<span>Add new room to space: </span>
+<FancyTextBox @bind-Value="@NewRoomId"></FancyTextBox>
+<button onclick="@AddNewRoom">Add</button>
<button onclick="@JoinAllRooms">Join all rooms</button>
@foreach (var room in Rooms) {
- <RoomListItem Room="room" ShowOwnProfile="true"></RoomListItem>
+ <RoomListItem RoomInfo="room" ShowOwnProfile="true"></RoomListItem>
}
@@ -26,12 +30,13 @@
private GenericRoom? Room { get; set; }
- private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>();
- private List<GenericRoom> Rooms { get; } = new();
+ 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; }
protected override async Task OnInitializedAsync() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
Room = hs.GetRoom(RoomId.Replace('~', '.'));
@@ -43,8 +48,20 @@
var roomId = stateEvent.StateKey;
var room = hs.GetRoom(roomId);
if (room is not null) {
- Rooms.Add(room);
+ Task.Run(async () => {
+ try {
+ Rooms.Add(new(Room, await room.GetFullStateAsListAsync()));
+ }
+ catch (MatrixException e) {
+ if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) {
+ Rooms.Add(new(Room) {
+ RoomName = "M_FORBIDDEN"
+ });
+ }
+ }
+ });
}
+
break;
}
case "m.room.member": {
@@ -52,52 +69,84 @@
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);
- // }
- // }
- // }
-
- // 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();
+ // 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!");
+ // }
+
+ // await base.OnInitializedAsync();
}
private async Task JoinAllRooms() {
// List<Task<RoomIdResponse>> tasks = Rooms.Select(room => room.JoinAsync(ServersInSpace.ToArray())).ToList();
// await Task.WhenAll(tasks);
foreach (var room in Rooms) {
- await room.JoinAsync(ServersInSpace.ToArray());
+ await JoinRecursive(room.Room.RoomId);
}
}
+ private async Task JoinRecursive(string roomId) {
+ var room = Room!.Homeserver.GetRoom(roomId);
+ if (room is null) return;
+ try {
+ await room.JoinAsync(ServersInSpace.Take(10).ToArray());
+ var joined = false;
+ while (!joined) {
+ var ce = await room.GetCreateEventAsync();
+ if (ce is null) continue;
+ if (ce.Type == "m.space") {
+ var children = room.AsSpace().GetChildrenAsync(false);
+ await foreach (var child in children) {
+ JoinRecursive(child.RoomId);
+ }
+ }
+
+ joined = true;
+ await Task.Delay(1000);
+ }
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ }
+ }
+
+ private async Task AddNewRoom() {
+ if (string.IsNullOrWhiteSpace(NewRoomId)) return;
+ await Room.AsSpace().AddChildByIdAsync(NewRoomId);
+ }
+
}
diff --git a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
index 6110b83..47146bc 100644
--- a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
+++ b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
@@ -37,13 +37,13 @@
[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() {
await base.OnInitializedAsync();
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
RoomId = RoomId.Replace('~', '.');
await LoadStatesAsync();
@@ -53,12 +53,12 @@
private DateTime _lastUpdate = DateTime.Now;
private async Task LoadStatesAsync() {
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
var StateLoaded = 0;
var response = (hs.GetRoom(RoomId)).GetFullStateAsync();
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 7c31136..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,13 +64,13 @@
[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() {
await base.OnInitializedAsync();
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
await LoadStatesAsync();
Console.WriteLine("Policy list editor initialized!");
@@ -80,7 +80,7 @@
private async Task LoadStatesAsync() {
var StateLoaded = 0;
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
var response = (hs.GetRoom(RoomId)).GetFullStateAsync();
await foreach (var _ev in response) {
@@ -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 e6b1248..f9137b0 100644
--- a/MatrixUtils.Web/Pages/Rooms/Timeline.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
@@ -2,7 +2,8 @@
@using MatrixUtils.Web.Shared.TimelineComponents
@using LibMatrix
@using LibMatrix.EventTypes.Spec
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Responses
<h3>RoomManagerTimeline</h3>
<hr/>
<p>Loaded @Events.Count events...</p>
@@ -21,13 +22,13 @@
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; }
protected override async Task OnInitializedAsync() {
Console.WriteLine("RoomId: " + RoomId);
- Homeserver = await RMUStorage.GetCurrentSessionOrNavigate();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (Homeserver is null) return;
var room = Homeserver.GetRoom(RoomId);
MessagesResponse? msgs = null;
@@ -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; }
}
|