diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj
index e61ef31..a72013b 100644
--- a/Benchmarks/Benchmarks.csproj
+++ b/Benchmarks/Benchmarks.csproj
@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
+ <PackageReference Include="BenchmarkDotNet" Version="0.15.3" />
</ItemGroup>
</Project>
diff --git a/LibMatrix b/LibMatrix
-Subproject 013f1693885a5de01ae357af2909589e925863d
+Subproject 91319ba62de889bde645b6f1df4dd6a960ee7de
diff --git a/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj b/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj
index 4d0299a..b0167e2 100644
--- a/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj
+++ b/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.8"/>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
</ItemGroup>
<ItemGroup>
diff --git a/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json b/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json
index f42db52..621d281 100644
--- a/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json
+++ b/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json
@@ -9,7 +9,7 @@
"LibMatrixBot": {
// Homeserver to connect to.
// Note: Homeserver resolution is applied here, but a direct base URL can be used.
- "Homeserver": "rory.gay",
+// "Homeserver": "rory.gay",
// Absolute path to the file containing the access token
"AccessTokenPath": "/home/Rory/matrix_access_token"
diff --git a/MatrixUtils.RoomUpgradeCLI/appsettings.SynapseDev.json b/MatrixUtils.RoomUpgradeCLI/appsettings.SynapseDev.json
deleted file mode 100644
index e69de29..0000000
--- a/MatrixUtils.RoomUpgradeCLI/appsettings.SynapseDev.json
+++ /dev/null
diff --git a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
index f446bf3..11781a8 100644
--- a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
+++ b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.8" />
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.9" />
</ItemGroup>
<ItemGroup>
diff --git a/MatrixUtils.Web/App.razor b/MatrixUtils.Web/App.razor
index a8cf817..7e8e1c3 100644
--- a/MatrixUtils.Web/App.razor
+++ b/MatrixUtils.Web/App.razor
@@ -1,4 +1,5 @@
-<Router AppAssembly="@typeof(App).Assembly">
+@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
+<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
@@ -10,3 +11,9 @@
</LayoutView>
</NotFound>
</Router>
+
+@code {
+
+ public static WebAssemblyHost Host { get; set; } = null!;
+
+}
diff --git a/MatrixUtils.Web/Classes/LocalStorageProviderService.cs b/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
index 0e99cd4..ddf3eed 100644
--- a/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
+++ b/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
@@ -3,26 +3,20 @@ using LibMatrix.Interfaces.Services;
namespace MatrixUtils.Web.Classes;
-public class LocalStorageProviderService : IStorageProvider {
- private readonly ILocalStorageService _localStorageService;
-
- public LocalStorageProviderService(ILocalStorageService localStorageService) {
- _localStorageService = localStorageService;
- }
-
+public class LocalStorageProviderService(ILocalStorageService localStorageService) : IStorageProvider {
Task IStorageProvider.SaveAllChildrenAsync<T>(string key, T value) {
throw new NotImplementedException();
}
Task<T?> IStorageProvider.LoadAllChildrenAsync<T>(string key) where T : default => throw new NotImplementedException();
- async Task IStorageProvider.SaveObjectAsync<T>(string key, T value) => await _localStorageService.SetItemAsync(key, value);
+ async Task IStorageProvider.SaveObjectAsync<T>(string key, T value) => await localStorageService.SetItemAsync(key, value);
- async Task<T?> IStorageProvider.LoadObjectAsync<T>(string key) where T : default => await _localStorageService.GetItemAsync<T>(key);
+ async Task<T?> IStorageProvider.LoadObjectAsync<T>(string key) where T : default => await localStorageService.GetItemAsync<T>(key);
- async Task<bool> IStorageProvider.ObjectExistsAsync(string key) => await _localStorageService.ContainKeyAsync(key);
+ async Task<bool> IStorageProvider.ObjectExistsAsync(string key) => await localStorageService.ContainKeyAsync(key);
- async Task<IEnumerable<string>> IStorageProvider.GetAllKeysAsync() => (await _localStorageService.KeysAsync()).ToList();
+ async Task<IEnumerable<string>> IStorageProvider.GetAllKeysAsync() => (await localStorageService.KeysAsync()).ToList();
- async Task IStorageProvider.DeleteObjectAsync(string key) => await _localStorageService.RemoveItemAsync(key);
+ async Task IStorageProvider.DeleteObjectAsync(string key) => await localStorageService.RemoveItemAsync(key);
}
diff --git a/MatrixUtils.Web/Classes/RmuSessionStore.cs b/MatrixUtils.Web/Classes/RmuSessionStore.cs
index 9df8837..1611b83 100644
--- a/MatrixUtils.Web/Classes/RmuSessionStore.cs
+++ b/MatrixUtils.Web/Classes/RmuSessionStore.cs
@@ -26,6 +26,11 @@ public class RmuSessionStore(
public async Task<SessionInfo?> GetSession(string sessionId) {
await LoadStorage();
+ if (string.IsNullOrEmpty(sessionId)) {
+ logger.LogWarning("No session ID provided.");
+ return null;
+ }
+
if (SessionCache.TryGetValue(sessionId, out var cachedSession))
return cachedSession;
@@ -39,6 +44,11 @@ public class RmuSessionStore(
if (CurrentSession is not null) return CurrentSession;
var currentSessionId = await storageService.DataStorageProvider!.LoadObjectAsync<string>("rmu.session");
+ if (currentSessionId == null) {
+ if (log) logger.LogWarning("No current session ID found in storage.");
+ return null;
+ }
+
return await GetSession(currentSessionId);
}
@@ -52,25 +62,31 @@ public class RmuSessionStore(
SessionId = sessionId
};
+ await SaveStorage();
if (CurrentSession == null) await SetCurrentSession(sessionId);
- else await SaveStorage();
return sessionId;
}
public async Task RemoveSession(string sessionId) {
await LoadStorage();
- logger.LogTrace("Removing session {sessionId}.", sessionId);
- var tokens = await GetAllSessions();
- if (tokens == null) {
+ if (SessionCache.Count == 0) {
+ logger.LogWarning("No sessions found.");
return;
}
+ logger.LogTrace("Removing session {sessionId}.", sessionId);
+
if ((await GetCurrentSession())?.SessionId == sessionId)
- await SetCurrentSession(tokens.First(x => x.Key != sessionId).Key);
+ await SetCurrentSession(SessionCache.FirstOrDefault(x => x.Key != sessionId).Key);
- if (tokens.Remove(sessionId))
- await SaveStorage();
+ if (SessionCache.Remove(sessionId)) {
+ logger.LogInformation("RemoveSession: Removed session {sessionId}.", sessionId);
+ logger.LogInformation("RemoveSession: Remaining sessions: {sessionIds}.", string.Join(", ", SessionCache.Keys));
+ await SaveStorage(log: true);
+ }
+ else
+ logger.LogWarning("RemoveSession: Session {sessionId} not found.", sessionId);
}
public async Task SetCurrentSession(string? sessionId) {
@@ -134,6 +150,53 @@ public class RmuSessionStore(
}
}
+ public async IAsyncEnumerable<AuthenticatedHomeserverGeneric> TryGetAllHomeservers(bool log = true, bool ignoreFailures = true) {
+ await LoadStorage();
+ if (log) logger.LogTrace("Getting all homeservers.");
+ var tasks = SessionCache.Values.Select(async session => {
+ if (ignoreFailures && session.Auth.LastFailureReason != null && session.Auth.LastFailureReason != UserAuth.FailureReason.None) {
+ if (log) logger.LogTrace("Skipping session {sessionId} due to previous failure: {reason}", session.SessionId, session.Auth.LastFailureReason);
+ return null;
+ }
+
+ try {
+ var hs = await GetHomeserver(session.SessionId, log: false);
+ if (session.Auth.LastFailureReason != null) {
+ SessionCache[session.SessionId].Auth.LastFailureReason = null;
+ await SaveStorage();
+ }
+
+ return hs;
+ }
+ catch (Exception e) {
+ logger.LogError("TryGetAllHomeservers: Failed to get homeserver for {userId} via {homeserver}: {ex}", session.Auth.UserId, session.Auth.Homeserver, e);
+ var reason = SessionCache[session.SessionId].Auth.LastFailureReason = e switch {
+ MatrixException { ErrorCode: MatrixException.ErrorCodes.M_UNKNOWN_TOKEN } => UserAuth.FailureReason.InvalidToken,
+ HttpRequestException => UserAuth.FailureReason.NetworkError,
+ _ => UserAuth.FailureReason.UnknownError
+ };
+ await SaveStorage(log: true);
+
+ // await LoadStorage(true);
+ if (SessionCache[session.SessionId].Auth.LastFailureReason != reason) {
+ await Console.Error.WriteLineAsync(
+ $"Warning: Session {session.SessionId} failure reason changed during reload from {reason} to {SessionCache[session.SessionId].Auth.LastFailureReason}");
+ }
+
+ throw;
+ }
+ }).ToList();
+
+ while (tasks.Count != 0) {
+ var finished = await Task.WhenAny(tasks);
+ tasks.Remove(finished);
+ if (finished.IsFaulted) continue;
+
+ var result = await finished;
+ if (result != null) yield return result;
+ }
+ }
+
#endregion
#region Storage
@@ -170,7 +233,8 @@ public class RmuSessionStore(
CurrentSession = currentSession;
}
- private async Task SaveStorage() {
+ private async Task SaveStorage(bool log = false) {
+ if (log) logger.LogWarning("Saving {count} sessions to storage.", SessionCache.Count);
await storageService.DataStorageProvider!.SaveObjectAsync("rmu.sessions",
SessionCache.ToDictionary(
x => x.Key,
@@ -178,6 +242,7 @@ public class RmuSessionStore(
)
);
await storageService.DataStorageProvider.SaveObjectAsync("rmu.session", CurrentSession?.SessionId);
+ if (log) logger.LogWarning("{count} sessions saved to storage.", SessionCache.Count);
}
#endregion
@@ -190,29 +255,42 @@ public class RmuSessionStore(
}
private async Task MigrateFromMru() {
- logger.LogInformation("Migrating from MRU token namespace!");
var dsp = storageService.DataStorageProvider!;
- if (await dsp.ObjectExistsAsync("token")) {
- var oldToken = await dsp.LoadObjectAsync<UserAuth>("token");
- if (oldToken != null) {
- await dsp.SaveObjectAsync("rmu.token", oldToken);
- await dsp.DeleteObjectAsync("token");
+ if (await dsp.ObjectExistsAsync("token") || await dsp.ObjectExistsAsync("tokens")) {
+ logger.LogInformation("Migrating from unnamespaced localstorage!");
+ if (await dsp.ObjectExistsAsync("token")) {
+ var oldToken = await dsp.LoadObjectAsync<UserAuth>("token");
+ if (oldToken != null) {
+ await dsp.SaveObjectAsync("mru.token", oldToken);
+ await dsp.DeleteObjectAsync("token");
+ }
}
- }
- if (await dsp.ObjectExistsAsync("tokens")) {
- var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("tokens");
- if (oldTokens != null) {
- await dsp.SaveObjectAsync("rmu.tokens", oldTokens);
- await dsp.DeleteObjectAsync("tokens");
+ if (await dsp.ObjectExistsAsync("tokens")) {
+ var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("tokens");
+ if (oldTokens != null) {
+ await dsp.SaveObjectAsync("mru.tokens", oldTokens);
+ await dsp.DeleteObjectAsync("tokens");
+ }
}
}
- if (await dsp.ObjectExistsAsync("mru.tokens")) {
- var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("mru.tokens");
- if (oldTokens != null) {
- await dsp.SaveObjectAsync("rmu.tokens", oldTokens);
- await dsp.DeleteObjectAsync("mru.tokens");
+ if (await dsp.ObjectExistsAsync("mru.token") || await dsp.ObjectExistsAsync("mru.tokens")) {
+ logger.LogInformation("Migrating from MRU token namespace!");
+ if (await dsp.ObjectExistsAsync("mru.token")) {
+ var oldToken = await dsp.LoadObjectAsync<UserAuth>("mru.token");
+ if (oldToken != null) {
+ await dsp.SaveObjectAsync("rmu.token", oldToken);
+ await dsp.DeleteObjectAsync("mru.token");
+ }
+ }
+
+ if (await dsp.ObjectExistsAsync("mru.tokens")) {
+ var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("mru.tokens");
+ if (oldTokens != null) {
+ await dsp.SaveObjectAsync("rmu.tokens", oldTokens);
+ await dsp.DeleteObjectAsync("mru.tokens");
+ }
}
}
}
diff --git a/MatrixUtils.Web/Classes/UserAuth.cs b/MatrixUtils.Web/Classes/UserAuth.cs
index 66476ae..16bb758 100644
--- a/MatrixUtils.Web/Classes/UserAuth.cs
+++ b/MatrixUtils.Web/Classes/UserAuth.cs
@@ -1,9 +1,11 @@
+using System.Text.Json.Serialization;
using LibMatrix.Responses;
namespace MatrixUtils.Web.Classes;
public class UserAuth : LoginResponse {
public UserAuth() { }
+
public UserAuth(LoginResponse login) {
Homeserver = login.Homeserver;
UserId = login.UserId;
@@ -12,4 +14,14 @@ public class UserAuth : LoginResponse {
}
public string? Proxy { get; set; }
-}
+
+ public FailureReason? LastFailureReason { get; set; }
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum FailureReason {
+ None,
+ InvalidToken,
+ NetworkError,
+ UnknownError
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj
index 18204d0..44fce2d 100644
--- a/MatrixUtils.Web/MatrixUtils.Web.csproj
+++ b/MatrixUtils.Web/MatrixUtils.Web.csproj
@@ -39,11 +39,12 @@
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0"/>
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0"/>
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.8" />
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.8" PrivateAssets="all" />
- <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.8" />
- <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.8" />
- <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.17.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.9" PrivateAssets="all" />
+ <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.9" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.9" />
+ <PackageReference Include="SpawnDev.BlazorJS" Version="2.29.0" />
+ <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.19.0" />
</ItemGroup>
<ItemGroup>
diff --git a/MatrixUtils.Web/Pages/Dev/DevOptions.razor b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
index 33e577f..281cf07 100644
--- a/MatrixUtils.Web/Pages/Dev/DevOptions.razor
+++ b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
@@ -21,7 +21,7 @@
</p>
<details>
<summary>Manage local sessions</summary>
-
+
</details>
@if (userSettings is not null) {
@@ -40,10 +40,14 @@
@code {
private RmuSessionStore.Settings? userSettings { get; set; }
+
protected override async Task OnInitializedAsync() {
- // userSettings = await TieredStorage.DataStorageProvider.LoadObjectAsync<RmuSessionStore.Settings>("rmu.settings");
-
- await base.OnInitializedAsync();
+ await (Task)typeof(RmuSessionStore).GetMethod("LoadStorage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.Invoke(sessionStore, [true])!;
+ await foreach (var _ in sessionStore.TryGetAllHomeservers()) { }
+
+ await (Task)typeof(RmuSessionStore).GetMethod("SaveStorage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.Invoke(sessionStore, [true])!;
}
private async Task LogStuff() {
@@ -58,8 +62,9 @@
foreach (var key in keys) {
data.Add(key, await TieredStorage.DataStorageProvider.LoadObjectAsync<object>(key));
}
+
var dataUri = "data:application/json;base64,";
- dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)));
+ dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)));
await JSRuntime.InvokeVoidAsync("window.open", dataUri, "_blank");
}
@@ -69,6 +74,7 @@
foreach (var (key, value) in data) {
await TieredStorage.DataStorageProvider.SaveObjectAsync(key, value);
}
+
NavigationManager.NavigateTo(NavigationManager.Uri, true, true);
}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
index 999e331..48aea86 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -105,6 +105,8 @@
}
public async Task DeleteRoom() {
+ var resp = await Homeserver.Admin.DeleteRoom(Context.RoomId, Context.DeleteRequest, false);
+ Context.DeleteId = resp.DeleteId;
await TaskMap.SetValueAsync(Context.RoomId, Context);
}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
index 5e45e5b..07a3dd2 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -34,21 +34,6 @@
@if (Results.Count > 0) {
<p>Found @Results.Count rooms</p>
- @* <details> *@
- @* <summary>TSV data (copy/paste)</summary> *@
- @* <pre style="font-size: 0.6em;"> *@
- @* <table> *@
- @* @foreach (var res in Results) { *@
- @* <tr> *@
- @* <td style="padding: 8px;">@res.RoomId@("\t")</td> *@
- @* <td style="padding: 8px;">@res.CanonicalAlias@("\t")</td> *@
- @* <td style="padding: 8px;">@res.Creator@("\t")</td> *@
- @* <td style="padding: 8px;">@res.Name</td> *@
- @* </tr> *@
- @* } *@
- @* </table> *@
- @* </pre> *@
- @* </details> *@
}
@foreach (var room in Results) {
@@ -71,12 +56,12 @@
</p>
<p>
<LinkButton OnClickAsync="@(() => DeleteRoom(room))">Delete room</LinkButton>
- <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={room.RoomId}&via={room.RoomId.Split(':', 2)[1]}")">Resync state</LinkButton>
+ <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={room.RoomId}&via={room.OriginHomeserver}")">Resync state</LinkButton>
</p>
@{
List<string?> flags = [];
- if (true || room.JoinedLocalMembers > 0) {
+ if (room.JoinedLocalMembers > 0) {
flags.Add(room.JoinRules switch {
"public" => "Public",
"invite" => "Invite only",
@@ -88,7 +73,7 @@
"" => null,
_ => "unknown join rule: " + room.JoinRules
});
-
+
if (!string.IsNullOrWhiteSpace(room.Encryption)) flags.Add("encrypted");
if (!room.Federatable) flags.Add("unfederated");
@@ -124,7 +109,8 @@
<span>@room.StateEvents state events, room version @(room.Version ?? "1")</span><br/>
@if (room.TombstoneEvent is not null) {
var tombstoneContent = room.TombstoneEvent.ContentAs<RoomTombstoneEventContent>()!;
- <span>Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body</span><br/>
+ <span>Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body</span>
+ <br/>
}
@{
@@ -283,6 +269,7 @@
private async Task Search() {
Results.Clear();
var searchRooms = Homeserver.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator();
+ var joinedRooms = await Homeserver.GetJoinedRooms();
while (await searchRooms.MoveNextAsync()) {
var room = searchRooms.Current;
@@ -300,7 +287,11 @@
HistoryVisibility = room.HistoryVisibility,
StateEvents = room.StateEvents,
JoinedMembers = room.JoinedMembers,
- JoinedLocalMembers = room.JoinedLocalMembers
+ JoinedLocalMembers = room.JoinedLocalMembers,
+ OriginHomeserver = joinedRooms.Any(x => x.RoomId == room.RoomId)
+ ? await Homeserver.GetRoom(room.RoomId).GetOriginHomeserverAsync()
+ : (await Homeserver.Admin.GetRoomStateAsync(room.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1]
+ ?? string.Empty
};
Results.Add(roomInfo);
@@ -425,6 +416,7 @@
private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom {
public List<string>? LocalMembers { get; set; }
public StateEventResponse? TombstoneEvent { get; set; }
+ public required string OriginHomeserver { get; set; }
[field: AllowNull, MaybeNull]
public string MemberSummary => field ??= $"{JoinedMembers} members, of which {JoinedLocalMembers} are on this server";
diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
index 79f931b..dd217e9 100644
--- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
+++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
@@ -36,7 +36,6 @@
}
//debounce StateHasChanged, we dont want to reredner on every key stroke
-
private CancellationTokenSource _debounceCts = new CancellationTokenSource();
private async Task DebouncedStateHasChanged() {
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index 412d8c9..5876861 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -5,22 +5,26 @@
@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
@if (!IsInitialised) {
<p>Connecting to homeserver...</p>
}
else {
- <PolicyListEditorHeader Room="@Room" ReloadStateAsync="@(() => LoadStateAsync(true))"></PolicyListEditorHeader>
+ <PolicyListEditorHeader Room="@Room" @bind-RenderEventInfo="@RenderEventInfo" ReloadStateAsync="@(() => LoadStateAsync(true))"></PolicyListEditorHeader>
@if (Loading) {
<p>Loading...</p>
}
@@ -39,14 +43,36 @@ else {
</p>
}
+ @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 PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent>
+ <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent>
}
// foreach (var type in policiesByType) {
@@ -119,7 +145,7 @@ else {
// }
// logger.LogInformation($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
- logger.LogInformation($"Rendered in {renderTotalSw.Elapsed}");
+ logger.LogInformation("Rendered in {TimeSpan}", renderTotalSw.Elapsed);
}
}
@@ -138,7 +164,13 @@ else {
public required string RoomId { get; set; }
[Parameter, SupplyParameterFromQuery]
- public bool RenderEventInfo { get; set; }
+ public bool RenderEventInfo {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
@@ -157,6 +189,19 @@ else {
public Dictionary<StateEventResponse, int> ActiveKicks { get; set; } = [];
+ private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+
+ // event types, unnamed
+ // private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
+ // .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+ //
+ // private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes
+ // .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray());
+
+ Dictionary<Type, PolicyCollection> PolicyCollections { get; set; } = new();
+ PolicyCollection? DuplicateBans { get; set; }
+ PolicyCollection? RedundantBans { get; set; }
+
protected override async Task OnInitializedAsync() {
var sw = Stopwatch.StartNew();
await base.OnInitializedAsync();
@@ -172,18 +217,19 @@ else {
StateHasChanged();
await LoadStateAsync(firstLoad: true);
Loading = false;
- logger.LogInformation($"Policy list editor initialized in {sw.Elapsed}!");
+ logger.LogInformation("Policy list editor initialized in {SwElapsed}!", sw.Elapsed);
}
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 {sw.Elapsed}");
+ logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed);
foreach (var type in KnownPolicyTypes) {
if (!PolicyCollections.ContainsKey(type)) {
@@ -197,7 +243,7 @@ else {
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($"{proxySafeProps?.Count} proxy safe props found in {type.FullName} ({filterPropSw.Elapsed})");
+ 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 = [],
@@ -210,9 +256,11 @@ else {
var count = 0;
var parseSw = Stopwatch.StartNew();
foreach (var evt in states) {
- var sw2 = Stopwatch.StartNew();
var mappedType = evt.MappedType;
- logger.LogInformation($"Processing state #{count++:000000} {evt.Type} @ {sw.Elapsed} (took {parseSw.Elapsed:c} so far to process)");
+ 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];
@@ -239,21 +287,24 @@ else {
}
}
- logger.LogInformation($"LoadStatesAsync: Processed state in {sw.Elapsed}");
+ logger.LogInformation("LoadStatesAsync: Processed state in {SwElapsed}", sw.Elapsed);
foreach (var collection in PolicyCollections) {
- logger.LogInformation($"Policy collection {collection.Key.FullName} has {collection.Value.ActivePolicies.Count} active and {collection.Value.RemovedPolicies.Count} removed policies.");
+ 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)
- .ToList();
+ // 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();
@@ -290,28 +341,71 @@ else {
// }
// }
- Console.WriteLine($"Scanning for redundant policies in {allPolicyInfos.Count} total policies... ({scanSw.Elapsed})");
+ int scanningPolicyCount = 0;
+ var aggregatedPolicies = PolicyCollections.Values
+ .Aggregate(new List<StateEventResponse>(), (acc, val) => {
+ acc.AddRange(val.ActivePolicies.Select(x => x.Value.Policy));
+ return acc;
+ });
+ Console.WriteLine($"Scanning for redundant policies in {aggregatedPolicies.Count} total policies... ({scanSw.Elapsed})");
List<Task<List<PolicyCollection.PolicyInfo>>> tasks = [];
// try to save some load...
- var policiesJson = JsonSerializer.Serialize(allPolicyInfos.Select(x => x.Policy));
- var ranges = Enumerable.Range(0, allPolicyInfos.Count).DistributeSequentially(WebWorkerService.MaxWorkerCount);
- foreach (var range in ranges)
- tasks.Add(WebWorkerService.TaskPool.Invoke(CheckDuplicatePoliciesAsync, policiesJson, range.First(), range.Last()));
-
+ 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.ToAsyncEnumerable()) {
- Console.WriteLine($"Main: got {modifiedPolicyInfos.Count} modified policies from worker, time: {scanSw.Elapsed}");
+ 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 = modifiedPolicyInfo.DuplicatedBy;
- original.MadeRedundantBy = modifiedPolicyInfo.MadeRedundantBy;
+ original.DuplicatedBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.DuplicatedBy.Any(y => StateEvent.Equals(x, y))).ToList();
+ original.MadeRedundantBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.MadeRedundantBy.Any(y => StateEvent.Equals(x, y))).ToList();
+ modifiedPolicyInfo.DuplicatedBy = modifiedPolicyInfo.MadeRedundantBy = []; // Early dereference
+ if (original.DuplicatedBy.Count > 0) {
+ if (!DuplicateBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!)))
+ DuplicateBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original);
+ }
+
+ if (original.MadeRedundantBy.Count > 0) {
+ if (!RedundantBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!)))
+ RedundantBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original);
+ }
+ // Console.WriteLine($"Memory usage: {Util.BytesToString(GC.GetTotalMemory(false))}");
}
- Console.WriteLine($"Processed {modifiedPolicyInfos.Count} modified policies in {scanSw.Elapsed}");
+ Console.WriteLine($"Main: Processed {modifiedPolicyInfos.Count} modified policies in {scanSw.Elapsed} (applied in {applySw.Elapsed})");
}
- Console.WriteLine($"Processed {allPolicyInfos.Count} policies in {scanSw.Elapsed}");
+ Console.WriteLine($"Processed {allPolicyInfos.Length} policies in {scanSw.Elapsed}");
// // scan for wildcard matches
// foreach (var policy in allPolicies) {
@@ -381,9 +475,17 @@ else {
}
[return: WorkerTransfer]
+ private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(SpawnDev.BlazorJS.JSObjects.String policiesJson, int start, int end) {
+ var policies = JsonSerializer.Deserialize<List<StateEventResponse>>(policiesJson.ValueOf());
+ Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.ValueOf().Length} bytes of JSON ({policies!.Count} policies)");
+ return await CheckDuplicatePoliciesAsync(policies!, start .. end);
+ }
+
+ [return: WorkerTransfer]
private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(string policiesJson, int start, int end) {
- Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.Length} bytes of JSON");
- return await CheckDuplicatePoliciesAsync(JsonSerializer.Deserialize<List<StateEventResponse>>(policiesJson), start .. end);
+ var policies = JsonSerializer.Deserialize<List<StateEventResponse>>(policiesJson);
+ Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.Length} bytes of JSON ({policies!.Count} policies)");
+ return await CheckDuplicatePoliciesAsync(policies!, start .. end);
}
[return: WorkerTransfer]
@@ -392,6 +494,8 @@ else {
[return: WorkerTransfer]
private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<StateEventResponse> policies, Range range) {
+ var sw = Stopwatch.StartNew();
+ var jsConsole = App.Host.Services.GetService<JsConsoleService>()!;
Console.WriteLine($"Processing policies in range {range} ({range.GetOffsetAndLength(policies.Count).Length}) with {policies.Count} total policies");
var allPolicies = policies
.Select(x => (Event: x, TypedContent: (x.TypedContent as PolicyRuleEventContent)!))
@@ -410,10 +514,23 @@ else {
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,
@@ -424,6 +541,8 @@ else {
// 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;
}
@@ -437,7 +556,7 @@ else {
var states = await Room.GetFullStateAsListAsync();
// PolicyEventsByType.Clear();
- logger.LogInformation($"LoadStatesAsync: Loaded state in {sw.Elapsed}");
+ logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed);
foreach (var type in KnownPolicyTypes) {
if (!PolicyEventsByType.ContainsKey(type))
@@ -463,7 +582,7 @@ else {
}
e4 = _spsw.Elapsed;
- logger.LogInformation($"[E] LoadStatesAsync: Processed state #{count++:000000} {state.Type} @ {sw.Elapsed} (e1={e1:c}, e2={e2:c}, e3={e3:c}, e4={e4:c}, e5={TimeSpan.Zero:c},t={_spsw.Elapsed:c})");
+ 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;
}
@@ -473,25 +592,25 @@ else {
targetPolicies.Add(state);
e6 = _spsw.Elapsed;
t = _spsw.Elapsed;
- logger.LogInformation($"[M] LoadStatesAsync: Processed state #{count++:000000} {state.Type} @ {sw.Elapsed} (e1={e1:c}, e2={e2:c}, e3={e3:c}, e4={e4:c}, e5={e5:c}, e6={e6:c},t={t:c})");
+ 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 #{count++:000000} {state.Type} @ {sw.Elapsed} (e1={e1:c}, e2={e2:c}, e3={TimeSpan.Zero:c}, e4={TimeSpan.Zero:c}, e5={TimeSpan.Zero:c}, e6={TimeSpan.Zero:c}, t={t:c})");
+ 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 {sw.Elapsed}");
+ 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 {sw.Elapsed}");
+ logger.LogInformation("LoadStatesAsync: yield finished in {SwElapsed}", sw.Elapsed);
}
private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
@@ -511,19 +630,9 @@ else {
private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
- private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
-
- // event types, unnamed
- private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
- .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
-
- private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes
- .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray());
-
- Dictionary<Type, PolicyCollection> PolicyCollections { get; set; } = new();
-
public struct PolicyCollection {
public required string Name { get; init; }
+ public SpecialViewType ViewType { get; init; }
public int TotalCount => ActivePolicies.Count + RemovedPolicies.Count;
public required Dictionary<(string Type, string StateKey), PolicyInfo> ActivePolicies { get; set; }
@@ -537,6 +646,12 @@ else {
public required List<StateEventResponse> MadeRedundantBy { get; set; }
public required List<StateEventResponse> DuplicatedBy { get; set; }
}
+
+ public enum SpecialViewType {
+ None,
+ Duplicates,
+ Redundant,
+ }
}
// private struct PolicyStats {
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
index b52e03f..932e0fe 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
@@ -10,43 +10,45 @@
<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>
}
- <th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var policy in PolicyCollection.ActivePolicies.Values.OrderBy(x => x.Policy.RawContent?["entity"]?.GetValue<string>())) {
- <PolicyListRowComponent RenderEventInfo="RenderEventInfo" PolicyInfo="@policy" PolicyCollection="@PolicyCollection" Room="@Room"></PolicyListRowComponent>
+ <PolicyListRowComponent PolicyCollectionStateHasChanged="@StateHasChanged" RenderEventInfo="RenderEventInfo" PolicyInfo="@policy" PolicyCollection="@PolicyCollection" Room="@Room"></PolicyListRowComponent>
}
</tbody>
</table>
- <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) {
+ @if (RenderInvalidSection) {
+ <details>
+ <summary>
+ <u>
+ @("Invalid " + PolicyCollection.Name.ToLower())
+ </u>
+ </summary>
+ <table class="table table-striped table-hover table-bordered align-middle">
+ <thead>
<tr>
- <td>@policy.Policy.StateKey</td>
- <td>
- <pre>@policy.Policy.RawContent.ToJson(true, false)</pre>
- </td>
+ <th>State key</th>
+ <th>Json contents</th>
</tr>
- }
- </tbody>
- </table>
- </details>
+ </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 {
@@ -60,6 +62,9 @@
[Parameter]
public bool RenderEventInfo { get; set; }
+ [Parameter]
+ public bool RenderInvalidSection { get; set; } = true;
+
protected override bool ShouldRender() {
// if (PolicyCollection is null) return false;
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
index e82f17d..8585561 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
@@ -12,14 +12,14 @@
<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
+ CurrentlyEditingEvent = new() { Type = "", RawContent = new() };
+ return Task.CompletedTask;
+ })">Create new policy
</LinkButton>
<LinkButton OnClickAsync="@(() => {
- MassCreatePolicies = true;
- return Task.CompletedTask;
- })">Create many new policies
+ MassCreatePolicies = true;
+ return Task.CompletedTask;
+ })">Create many new policies
</LinkButton>
<LinkButton OnClickAsync="@(() => ReloadStateAsync())">Refresh</LinkButton>
@@ -33,20 +33,43 @@
// _ = LoadStatesAsync();
})"></MassPolicyEditorModal>
}
+<br/>
+<InputCheckbox Value="@RenderEventInfo" ValueChanged="@RenderEventInfoChanged" ValueExpression="@(() => RenderEventInfo)"/>
+<span> Render event info</span>
@code {
+
[Parameter]
public required GenericRoom Room { get; set; }
-
+
[Parameter]
public required Func<Task> ReloadStateAsync { get; set; }
+ [Parameter]
+ public required bool RenderEventInfo { get; set; }
+
+ [Parameter]
+ public required EventCallback<bool> RenderEventInfoChanged { get; set; }
+
private string? RoomName { get; set; }
private string? RoomAlias { get; set; }
private string? DraupnirShortcode { get; set; }
-
- private StateEventResponse? CurrentlyEditingEvent { get; set { field = value; StateHasChanged(); } }
- private bool MassCreatePolicies { get; set { field = value; StateHasChanged(); } }
+
+ private StateEventResponse? CurrentlyEditingEvent {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private bool MassCreatePolicies {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
protected override async Task OnInitializedAsync() {
await Task.WhenAll(
@@ -54,12 +77,12 @@
Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }),
Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); })
);
-
+
StateHasChanged();
}
private async Task UpdatePolicyAsync(StateEventResponse evt) {
- Console.WriteLine("UpdatePolicyAsync in PolicyListEditorHeader not yet implementeD!");
+ 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
index 9ac5077..cd432c9 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor
@@ -1,4 +1,5 @@
@using System.Reflection
+@using ArcaneLibs.Extensions
@using LibMatrix
@using LibMatrix.EventTypes.Spec.State.Policy
@using LibMatrix.RoomTypes
@@ -6,30 +7,6 @@
@if (_isInitialized && IsVisible) {
<tr id="@PolicyInfo.Policy.EventId">
- @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>
- @PolicyInfo.Policy.Type/@PolicyInfo.Policy.StateKey by @PolicyInfo.Policy.Sender at @PolicyInfo.Policy.OriginServerTs
- </pre>
- }
- </td>
- }
- else {
- <td>@prop.GetGetMethod()?.Invoke(TypedContent, null)</td>
- }
- }
<td>
<div style="display: flex; flex-direction: row; gap: 0.5em;">
@* @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, Policy.Type)) { *@
@@ -41,7 +18,7 @@
</LinkButton>
<LinkButton OnClickAsync="@RemovePolicyAsync">Remove</LinkButton>
@if (Policy.IsLegacyType) {
- <LinkButton OnClickAsync="@RemovePolicyAsync">Update policy type</LinkButton>
+ <LinkButton OnClickAsync="@RemovePolicyAsync">Update type</LinkButton>
}
@if (TypedContent.Entity?.StartsWith("@*:", StringComparison.Ordinal) == true) {
@@ -66,6 +43,30 @@
}
</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) {
@@ -95,6 +96,9 @@
[Parameter]
public bool RenderEventInfo { get; set; }
+ [Parameter]
+ public required Action PolicyCollectionStateHasChanged { get; set; }
+
private StateEventResponse Policy => PolicyInfo.Policy;
private bool IsEditing {
@@ -142,8 +146,42 @@
private async Task RemovePolicyAsync() {
await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, new { });
- IsVisible = false;
- StateHasChanged();
+ bool shouldUpdateVisibility = true;
+ PolicyCollection.ActivePolicies.Remove((Policy.Type, Policy.StateKey));
+ PolicyCollection.RemovedPolicies.Add((Policy.Type, Policy.StateKey), PolicyInfo);
+ if (PolicyInfo.DuplicatedBy.Count > 0) {
+ foreach (var evt in PolicyInfo.DuplicatedBy) {
+ var matchingEntry = PolicyCollection.ActivePolicies
+ .FirstOrDefault(x => StateEvent.Equals(x.Value.Policy, evt)).Value;
+ var removals = matchingEntry.DuplicatedBy.RemoveAll(x => StateEvent.Equals(x, Policy));
+ Console.WriteLine($"Removed {removals} duplicates from {evt.EventId}, matching entry: {matchingEntry.ToJson()}");
+ if (PolicyCollection.ViewType == PolicyList.PolicyCollection.SpecialViewType.Duplicates && matchingEntry.DuplicatedBy.Count == 0) {
+ PolicyCollection.ActivePolicies.Remove((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey));
+ PolicyCollection.RemovedPolicies.Add((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey), matchingEntry);
+ Console.WriteLine($"Also removed {matchingEntry.Policy.EventId} as it is now redundant");
+ }
+ }
+
+ PolicyCollectionStateHasChanged();
+ shouldUpdateVisibility = false;
+ }
+
+ if (PolicyInfo.MadeRedundantBy.Count > 0) {
+ foreach (var evt in PolicyInfo.MadeRedundantBy) {
+ var matchingEntry = PolicyCollection.ActivePolicies
+ .FirstOrDefault(x => StateEvent.Equals(x.Value.Policy, evt)).Value;
+ var removals = matchingEntry.MadeRedundantBy.RemoveAll(x => StateEvent.Equals(x, Policy));
+ Console.WriteLine($"Removed {removals} redundants from {evt.EventId}, matching entry: {matchingEntry.ToJson()}");
+ }
+
+ PolicyCollectionStateHasChanged();
+ shouldUpdateVisibility = false;
+ }
+
+ if (shouldUpdateVisibility) {
+ IsVisible = false;
+ StateHasChanged();
+ }
// PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
// await LoadStatesAsync();
}
diff --git a/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor
index 86a4c13..fc9c9bf 100644
--- a/MatrixUtils.Web/Pages/Rooms/Space.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Space.razor
@@ -120,7 +120,7 @@
var room = Room!.Homeserver.GetRoom(roomId);
if (room is null) return;
try {
- await room.JoinAsync(ServersInSpace.ToArray());
+ await room.JoinAsync(ServersInSpace.Take(10).ToArray());
var joined = false;
while (!joined) {
var ce = await room.GetCreateEventAsync();
@@ -132,6 +132,7 @@
}
}
joined = true;
+ await Task.Delay(1000);
}
}
catch (Exception e) {
diff --git a/MatrixUtils.Web/Pages/Tools/Index.razor b/MatrixUtils.Web/Pages/Tools/Index.razor
index 4a44753..a0abcd4 100644
--- a/MatrixUtils.Web/Pages/Tools/Index.razor
+++ b/MatrixUtils.Web/Pages/Tools/Index.razor
@@ -12,6 +12,7 @@
<a href="/Tools/User/MassRoomJoin">Join room across all session</a><br/>
<a href="/Tools/User/CopyPowerlevel">Copy highest powerlevel across all session</a><br/>
<a href="/Tools/User/ViewAccountData">View account data</a><br/>
+<a href="/Tools/User/StickerManager">Manage custom stickers and emojis</a><br/>
<h4 class="tool-category">Room tools</h4>
<hr/>
diff --git a/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor b/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor
new file mode 100644
index 0000000..a47d7f5
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor
@@ -0,0 +1,204 @@
+@page "/Tools/Room/SpacePermissions"
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.RoomTypes
+@using MatrixUtils.Web.Pages.Rooms
+<h3>Space Permissions</h3>
+<hr/>
+<span>Space ID: </span>
+<FancyTextBox @bind-Value="@SpaceId"/>
+<LinkButton OnClickAsync="@Execute">Execute</LinkButton>
+<br/>
+<InputCheckbox @bind-Value="@AutoRecurseSpaces"/>
+<span> Auto-recurse into child spaces</span>
+<br/>
+
+@if (RoomPowerLevels.Count == 0) {
+ <p>No data loaded.</p>
+}
+else {
+ <span>Loaded @LoadedSpaceRooms.Count spaces.</span>
+ <br/>
+ @if (SpaceRooms.Count > 0) {
+ <h3>Load more spaces:</h3>
+ @foreach (var room in SpaceRooms) {
+ <LinkButton OnClickAsync="@(() => LoadSpaceAsync(room.Key))">@room.Value</LinkButton>
+ }
+ }
+
+ <h3>By event type:</h3>
+ <table class="table-striped table-hover table-bordered align-middle">
+ <thead>
+ <td>Room</td>
+ @foreach (var key in OrderedEventTypes) {
+ <td>@key.Key
+ <br/>
+ ~ @Math.Round(key.Value, 2)
+ </td>
+ }
+ </thead>
+ <tbody>
+ @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Events!.Values.Average())) {
+ <tr>
+ <td>@roomName</td>
+ @foreach (var eventType in OrderedEventTypes) {
+ if (!powerLevels.Events!.ContainsKey(eventType.Key)) {
+ <td style="background-color: #ff000044;">-</td>
+ continue;
+ }
+
+ <td>@(powerLevels.Events![eventType.Key])</td>
+ }
+ </tr>
+ }
+ </tbody>
+ </table>
+ <br/>
+ <h3>By user:</h3>
+ <table class="table-striped table-hover table-bordered align-middle">
+ <thead>
+ <td>Room</td>
+ @foreach (var key in OrderedUsers) {
+ <td>@key.Key
+ <br/>
+ ~ @Math.Round(key.Value, 2)
+ </td>
+ }
+ </thead>
+ <tbody>
+ @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Users!.Values.Average())) {
+ <tr>
+ <td>@roomName</td>
+ @foreach (var eventType in OrderedUsers) {
+ if (!powerLevels.Users!.ContainsKey(eventType.Key)) {
+ <td style="background-color: #ff000044;">-</td>
+ continue;
+ }
+
+ <td>@(powerLevels.Users![eventType.Key])</td>
+ }
+ </tr>
+ }
+ </tbody>
+ </table>
+}
+
+@code {
+
+ [Parameter, SupplyParameterFromQuery]
+ public string? SpaceId { get; set; }
+
+ [Parameter, SupplyParameterFromQuery]
+ public bool AutoRecurseSpaces { get; set; }
+
+ private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+ private List<AuthenticatedHomeserverGeneric> AllHomeservers { get; set; } = [];
+ private Dictionary<string, List<GenericRoom>> JoinedHomeserversByRoom { get; set; } = [];
+
+ private Dictionary<string, RoomPowerLevelEventContent> RoomPowerLevels { get; set; } = [];
+ private Dictionary<string, string> SpaceRooms { get; set; } = [];
+ private List<string> LoadedSpaceRooms { get; set; } = [];
+
+ private Dictionary<string, double> OrderedEventTypes { get; set; } = new();
+ private Dictionary<string, double> OrderedUsers { get; set; } = new();
+
+ protected override async Task OnInitializedAsync() {
+ if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not AuthenticatedHomeserverGeneric hs) return;
+ Homeserver = hs;
+ await foreach (var server in sessionStore.TryGetAllHomeservers()) {
+ AllHomeservers.Add(server);
+ var joinedRooms = await server.GetJoinedRooms();
+ foreach (var room in joinedRooms) {
+ if (!JoinedHomeserversByRoom.ContainsKey(room.RoomId)) {
+ JoinedHomeserversByRoom[room.RoomId] = [];
+ }
+
+ JoinedHomeserversByRoom[room.RoomId].Add(room);
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(SpaceId)) {
+ await Execute();
+ }
+ }
+
+ private async Task Execute() {
+ RoomPowerLevels = [];
+ SpaceRooms = [];
+ await LoadSpaceAsync(SpaceId);
+ }
+
+ private async Task<GenericRoom> GetJoinedRoomAsync(string roomId) {
+ var room = Homeserver.GetRoom(roomId);
+ if (await room.IsJoinedAsync()) return room;
+
+ if (JoinedHomeserversByRoom.TryGetValue(roomId, out var rooms)) {
+ foreach (var r in rooms) {
+ if (await r.IsJoinedAsync()) return r;
+ }
+ }
+
+ foreach (var hs in AllHomeservers) {
+ if (hs == Homeserver) continue;
+ room = hs.GetRoom(roomId);
+ if (await room.IsJoinedAsync()) return room;
+ }
+
+ Console.WriteLine($"Not joined to room {roomId} on any known homeserver.");
+ return room; // not null, in case we can preview the room
+ }
+
+ private async Task LoadSpaceAsync(string spaceId) {
+ LoadedSpaceRooms.Add(spaceId);
+ SpaceRooms.Remove(spaceId);
+
+ var space = (await GetJoinedRoomAsync(spaceId)).AsSpace();
+ RoomPowerLevels[await space.GetNameOrFallbackAsync()] = AddFakeEvents(await space.GetPowerLevelsAsync());
+ var children = space.GetChildrenAsync();
+ await foreach (var childRoom in children) {
+ var child = await GetJoinedRoomAsync(childRoom.RoomId);
+ try {
+ var powerlevels = await child.GetPowerLevelsAsync();
+ RoomPowerLevels[await child.GetNameOrFallbackAsync()] = AddFakeEvents(powerlevels!);
+ if (await child.GetRoomType() == SpaceRoom.TypeName) {
+ if (AutoRecurseSpaces)
+ await LoadSpaceAsync(child.RoomId);
+ else
+ SpaceRooms.Add(child.RoomId, await child.GetNameOrFallbackAsync());
+ }
+
+ OrderedEventTypes = RoomPowerLevels
+ .SelectMany(x => x.Value.Events!)
+ .GroupBy(x => x.Key)
+ .ToDictionary(x => x.Key, x => x.Average(y => y.Value))
+ .OrderByDescending(x => x.Value)
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ OrderedUsers = RoomPowerLevels
+ .SelectMany(x => x.Value.Users!)
+ .GroupBy(x => x.Key)
+ .ToDictionary(x => x.Key, x => x.Average(y => y.Value))
+ .OrderByDescending(x => x.Value)
+ .ToDictionary(x => x.Key, x => x.Value);
+ StateHasChanged();
+ }
+ catch (Exception ex) {
+ Console.WriteLine($"Failed to get power levels for room {child.RoomId}: {ex}");
+ }
+ }
+ }
+
+ private RoomPowerLevelEventContent AddFakeEvents(RoomPowerLevelEventContent powerlevels) {
+ powerlevels.Events ??= [];
+ powerlevels.Events["[user_default]"] = powerlevels.UsersDefault ?? 0;
+ powerlevels.Events["[event_default]"] = powerlevels.EventsDefault ?? 0;
+ powerlevels.Events["[state_default]"] = powerlevels.StateDefault ?? 100;
+ powerlevels.Events["[ban]"] = powerlevels.Ban ?? 100;
+ powerlevels.Events["[invite]"] = powerlevels.Invite ?? 100;
+ powerlevels.Events["[kick]"] = powerlevels.Kick ?? 100;
+ powerlevels.Events["[ping_room]"] = powerlevels.NotificationsPl?.Room ?? 100;
+ powerlevels.Events["[redact]"] = powerlevels.Redact ?? 100;
+ return powerlevels;
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor
new file mode 100644
index 0000000..984130f
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor
@@ -0,0 +1,80 @@
+@page "/Tools/User/StickerManager"
+@using System.Diagnostics
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Common
+@using LibMatrix.EventTypes.Spec
+@inject ILogger<StickerManager> Logger
+<h3>Sticker/emoji manager</h3>
+
+@if (TotalStepsProgress is not null) {
+ <SimpleProgressIndicator ObservableProgress="@TotalStepsProgress"/>
+ <br/>
+}
+@if (_observableProgressState is not null) {
+ <SimpleProgressIndicator ObservableProgress="@_observableProgressState"/>
+ <br/>
+}
+
+@code {
+
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!;
+ private Msc2545EmoteRoomsAccountDataEventContent? EnabledEmoteRooms { get; set; }
+ private Dictionary<string, StickerRoom> StickerRooms { get; set; } = [];
+
+ private SimpleProgressIndicator.ObservableProgressState? _observableProgressState;
+
+ private SimpleProgressIndicator.ObservableProgressState? TotalStepsProgress { get; set; } = new() {
+ Label = "Authenticating with Matrix...",
+ Max = 2,
+ Value = 0
+ };
+
+ protected override async Task OnInitializedAsync() {
+ if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not { } hs)
+ return;
+ Homeserver = hs;
+ TotalStepsProgress?.Next("Fetching enabled emote packs...");
+ _ = hs.GetAccountDataOrNullAsync<Msc2545EmoteRoomsAccountDataEventContent>(Msc2545EmoteRoomsAccountDataEventContent.EventId)
+ .ContinueWith(r => {
+ EnabledEmoteRooms = r.Result;
+ StateHasChanged();
+ });
+
+ TotalStepsProgress?.Next("Getting joined rooms...");
+ _observableProgressState = new() {
+ Label = "Loading rooms...",
+ Max = 1,
+ Value = 0
+ };
+ var rooms = await hs.GetJoinedRooms();
+ _observableProgressState.Max.Value = rooms.Count;
+ StateHasChanged();
+
+ var ss = new SemaphoreSlim(32, 32);
+ var ss1 = new SemaphoreSlim(1, 1);
+ var roomScanTasks = rooms.Select(async room => {
+ // await Task.Delay(Random.Shared.Next(100, 1000 + (rooms.Count * 100)));
+ // await ss.WaitAsync();
+ var state = await room.GetFullStateAsListAsync();
+ StickerRoom sr = new();
+ foreach (var evt in state) {
+ if (evt.Type == RoomEmotesEventContent.EventId) { }
+ }
+
+ // ss.Release();
+ // await ss1.WaitAsync();
+ Console.WriteLine("Got state for room " + room.RoomId);
+ // _observableProgressState.Next($"Got state for room {room.RoomId}");
+ // await Task.Delay(1);
+ // ss1.Release();
+ return room.RoomId;
+ })
+ .ToList();
+ await foreach (var roomScanResult in roomScanTasks.ToAsyncEnumerable()) {
+ _observableProgressState.Label.Value = roomScanResult;
+ }
+ }
+
+ private class StickerRoom { }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Program.cs b/MatrixUtils.Web/Program.cs
index bc047e8..e48782f 100644
--- a/MatrixUtils.Web/Program.cs
+++ b/MatrixUtils.Web/Program.cs
@@ -82,5 +82,8 @@ MatrixHttpClient.LogRequests = false;
builder.Services.AddRoryLibMatrixServices();
builder.Services.AddScoped<RmuSessionStore>();
builder.Services.AddSingleton<BlazorSaveFileService>();
+builder.Services.AddSingleton<JsConsoleService>();
+
// await builder.Build().RunAsync();
-await builder.Build().BlazorJSRunAsync();
\ No newline at end of file
+var host = App.Host = builder.Build();
+await host.BlazorJSRunAsync();
\ No newline at end of file
diff --git a/MatrixUtils.Web/appsettings.Development.json b/MatrixUtils.Web/appsettings.Development.json
index 826edbf..1555d4e 100644
--- a/MatrixUtils.Web/appsettings.Development.json
+++ b/MatrixUtils.Web/appsettings.Development.json
@@ -4,6 +4,8 @@
"Default": "Trace",
"System": "Information",
"Microsoft": "Information",
+ "Microsoft.AspNetCore.StaticAssets": "Warning",
+ "Microsoft.AspNetCore.EndpointMiddleware": "Warning",
"ArcaneLibs.Blazor.Components.AuthorizedImage": "Information"
}
}
diff --git a/MatrixUtils.Web/wwwroot/css/app.css b/MatrixUtils.Web/wwwroot/css/app.css
index 3fac9ca..4511b3a 100644
--- a/MatrixUtils.Web/wwwroot/css/app.css
+++ b/MatrixUtils.Web/wwwroot/css/app.css
@@ -1,6 +1,11 @@
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
@import url('jetbrains-mono/jetbrains-mono.css');
+:root {
+ /*--bs-table-hover-bg: rgba(0, 0, 0, 0.75);*/
+ --bs-table-hover-bg: #FF00FF;
+}
+
.avatar48 {
width: 48px;
height: 48px;
|