From 89a14526658e5d061b1aef34ab569e979c9c0cf8 Mon Sep 17 00:00:00 2001 From: Rory& Date: Wed, 6 Aug 2025 03:15:16 +0200 Subject: Various changes, room create/upgrade work --- MatrixUtils.Web/Pages/Rooms/Create2.razor | 159 +++++ MatrixUtils.Web/Pages/Rooms/PolicyList.razor | 696 ++++++++++----------- MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs | 144 +++++ MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css | 9 - MatrixUtils.Web/Pages/Rooms/PolicyList2.razor | 12 +- .../PolicyListCategoryComponent.razor | 66 ++ .../PolicyListEditorHeader.razor | 65 ++ .../PolicyListRowComponent.razor | 167 +++++ MatrixUtils.Web/Pages/Rooms/PolicyLists.razor | 160 +++-- MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css | 6 - .../RoomCreateBasicRoomInfoOptions.razor | 52 ++ .../RoomCreateCreateOptions.razor | 92 +++ .../RoomCreateInitialStateOptions.razor | 52 ++ .../RoomCreateMembershipOptions.razor | 55 ++ .../RoomCreatePermissionsOptions.razor | 123 ++++ .../RoomCreatePrivacyOptions.razor | 70 +++ .../RoomCreateUpgradeOptions.razor | 33 + MatrixUtils.Web/Pages/Rooms/Timeline.razor | 1 + 18 files changed, 1513 insertions(+), 449 deletions(-) create mode 100644 MatrixUtils.Web/Pages/Rooms/Create2.razor create mode 100644 MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs delete mode 100644 MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css create mode 100644 MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor create mode 100644 MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor create mode 100644 MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor delete mode 100644 MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css create mode 100644 MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor create mode 100644 MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor create mode 100644 MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor create mode 100644 MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor create mode 100644 MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor create mode 100644 MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor create mode 100644 MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor (limited to 'MatrixUtils.Web/Pages/Rooms') diff --git a/MatrixUtils.Web/Pages/Rooms/Create2.razor b/MatrixUtils.Web/Pages/Rooms/Create2.razor new file mode 100644 index 0000000..19f6fbc --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Create2.razor @@ -0,0 +1,159 @@ +@page "/Rooms/Create2" +@using System.Text.Json +@using System.Reflection +@using ArcaneLibs +@using ArcaneLibs.Extensions +@using Blazored.LocalStorage +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Helpers +@using LibMatrix.Responses +@using MatrixUtils.Web.Classes.RoomCreationTemplates +@using MatrixUtils.Web.Pages.Rooms.RoomCreateComponents +@inject ILogger logger +@* @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not */ *@ + +

Room Manager - Create Room

+ +@if (Ready) { + + + @if (roomBuilder is RoomUpgradeBuilder roomUpgrade) { + + } + else { + @* *@ + @* *@ + @* *@ + @* *@ + } + + + + + + @* Initial states, should remain at bottom *@ +
Preset: *@ + @* @if (Presets is null) { *@ + @*

Presets is null!

*@ + @* } *@ + @* else { *@ + @*

Support for presets is currently disabled!

*@ + @* $1$ #1# *@ + @* $1$ @foreach (var createRoomRequest in Presets) { #1# *@ + @* $1$ #1# *@ + @* $1$ } #1# *@ + @* $1$ #1# *@ + @* } *@ + @*
+ Create room +
+
+
+ RoomBuilder state + + Show null values
+
+                @roomBuilder.ToJson(ignoreNull: !ShowNullInState)
+            
+
+
+} +@if (_matrixException is not null) { + +
+            @_matrixException.Message
+        
+
+} + +@code { + +#region State + + [Parameter, SupplyParameterFromQuery(Name = "previousRoomId")] + public string? PreviousRoomId { get; set; } + + private bool ShowNullInState { 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? 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(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/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor index 96879b8..cdb5894 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor @@ -8,190 +8,118 @@ @using System.Reflection @using ArcaneLibs.Attributes @using LibMatrix.EventTypes -@using LibMatrix.EventTypes.Common @using LibMatrix.EventTypes.Interop.Draupnir @using LibMatrix.EventTypes.Spec.State.RoomInfo - -@using MatrixUtils.Web.Shared.PolicyEditorComponents @using SpawnDev.BlazorJS.WebWorkers +@using MatrixUtils.Web.Pages.Rooms.PolicyListComponents @inject WebWorkerService WebWorkerService +@inject ILogger logger -

Policy list editor - Editing @(RoomName ?? RoomId)

-@if (!string.IsNullOrWhiteSpace(DraupnirShortcode)) { - Shortcode: @DraupnirShortcode -} -@if (!string.IsNullOrWhiteSpace(RoomAlias)) { - Alias: @RoomAlias -} -
-@* *@ -Create new policy - -Create many new policies - -Refresh - -@if (Loading) { -

Loading...

-} -else if (PolicyEventsByType is not { Count: > 0 }) { -

No policies yet

+@if (!IsInitialised) { +

Connecting to homeserver...

} else { - var renderSw = Stopwatch.StartNew(); - var renderTotalSw = Stopwatch.StartNew(); - @foreach (var (type, value) in PolicyEventsByType) { -

- @(GetValidPolicyEventsByType(type).Count) active, - @(GetInvalidPolicyEventsByType(type).Count) invalid, - @(GetRemovedPolicyEventsByType(type).Count) removed - (@value.Count total) - @(GetPolicyTypeName(type).ToLower()) -

- } - - Console.WriteLine($"Rendered header in {renderSw.GetElapsedAndRestart()}"); - - var renderSw2 = Stopwatch.StartNew(); - IOrderedEnumerable policiesByType = KnownPolicyTypes.Where(t => GetPolicyEventsByType(t).Count > 0).OrderByDescending(t => GetPolicyEventsByType(t).Count); - Console.WriteLine($"Ordered policy types by count in {renderSw2.GetElapsedAndRestart()}"); - - foreach (var type in policiesByType) { -
- - - @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") - -
-
- - @{ - 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() 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()}"); - Console.WriteLine($"Filtered policies and got properties in {renderSw3.GetElapsedAndRestart()}"); - } - - - @foreach (var name in propNames) { - - } - - - - - @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue())) { - - @{ - var typedContent = policy.TypedContent! as PolicyRuleEventContent; - } - @foreach (var prop in proxySafeProps ?? Enumerable.Empty()) { - if (prop.Name == "Entity") { - - } - else { - - } - } - - - } - -
@nameActions
@TruncateMxid(typedContent!.Entity)@prop.GetGetMethod()?.Invoke(typedContent, null) -
- @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) { - Edit - - Remove - @if (policy.IsLegacyType) { - Update policy type - } - - @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.Type)) { - Make permanent - - @if (CurrentUserIsDraupnir) { - Kick - users @(ActiveKicks.TryGetValue(policy, out var kick) ? $"({kick})" : null) - - } - } - } - else { -

No permission to modify

- } -
-
-
- - - @("Invalid " + GetPolicyTypeName(type).ToLower()) - - - - - - - - - - - @foreach (var policy in invalidPolicies) { - - - - - } - -
State keyJson contents
@policy.StateKey -
@policy.RawContent.ToJson(true, false)
-
-
-
+ + @if (Loading) { +

Loading...

} + // else if (PolicyEventsByType is not { Count: > 0 }) { + @*

No policies yet

*@ + // } + else { + var renderSw = Stopwatch.StartNew(); + var renderTotalSw = Stopwatch.StartNew(); + @foreach (var value in PolicyCollections.Values.OrderByDescending(x => x.TotalCount)) { +

+ @value.ActivePolicies.Count active, + @value.RemovedPolicies.Count removed + (@value.TotalCount total) + @value.Name.ToLower() +

+ } - Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); - Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}"); -} + // logger.LogInformation($"Rendered header in {renderSw.GetElapsedAndRestart()}"); -@if (CurrentlyEditingEvent is not null) { - -} + // var renderSw2 = Stopwatch.StartNew(); + // IOrderedEnumerable policiesByType = KnownPolicyTypes.Where(t => GetPolicyEventsByType(t).Count > 0).OrderByDescending(t => GetPolicyEventsByType(t).Count); + // logger.LogInformation($"Ordered policy types by count in {renderSw2.GetElapsedAndRestart()}"); -@if (ServerPolicyToMakePermanent is not null) { - - - -} + foreach (var collection in PolicyCollections.Values.OrderByDescending(x => x.ActivePolicies.Count)) { + + } -@if (MassCreatePolicies) { - + // foreach (var type in policiesByType) { + @* foreach (var type in (List) []) { *@ + @*
*@ + @* *@ + @* *@ + @* @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") *@ + @* *@ + @*
*@ + @*
*@ + @* *@ + @* @{ *@ + @* 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() 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()}"); *@ + @* } *@ + @* *@ + @* *@ + @* @foreach (var name in propNames) { *@ + @* *@ + @* } *@ + @* *@ + @* *@ + @* *@ + @* *@ + @* @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue())) { *@ + @* *@ + @* } *@ + @* *@ + @*
@nameActions
*@ + @*
*@ + @* *@ + @* *@ + @* @("Invalid " + GetPolicyTypeName(type).ToLower()) *@ + @* *@ + @* *@ + @* *@ + @* *@ + @* *@ + @* *@ + @* *@ + @* *@ + @* *@ + @* *@ + @* @foreach (var policy in invalidPolicies) { *@ + @* *@ + @* *@ + @* *@ + @* *@ + @* } *@ + @* *@ + @*
State keyJson contents
@policy.StateKey *@ + @*
@policy.RawContent.ToJson(true, false)
*@ + @*
*@ + @*
*@ + @*
*@ + // } + + // logger.LogInformation($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); + logger.LogInformation($"Rendered in {renderTotalSw.Elapsed}"); + } } @code { @@ -202,6 +130,7 @@ else { private const bool Debug = false; #endif + private bool IsInitialised { get; set; } = false; private bool Loading { get; set; } = true; [Parameter] @@ -209,14 +138,6 @@ else { private Dictionary> PolicyEventsByType { get; set; } = new(); - private StateEventResponse? CurrentlyEditingEvent { - get; - set { - field = value; - StateHasChanged(); - } - } - public StateEventResponse? ServerPolicyToMakePermanent { get; set { @@ -225,22 +146,12 @@ else { } } - private AuthenticatedHomeserverGeneric Homeserver { get; set; } - private GenericRoom Room { get; set; } - private RoomPowerLevelEventContent PowerLevels { get; set; } + private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; + private GenericRoom Room { get; set; } = null!; + private RoomPowerLevelEventContent PowerLevels { get; set; } = null!; public bool CurrentUserIsDraupnir { get; set; } - public string? RoomName { get; set; } - public string? RoomAlias { get; set; } - public string? DraupnirShortcode { get; set; } - public Dictionary ActiveKicks { get; set; } = []; - public bool MassCreatePolicies { - get; - set { - field = value; - StateHasChanged(); - } - } + public Dictionary ActiveKicks { get; set; } = []; protected override async Task OnInitializedAsync() { var sw = Stopwatch.StartNew(); @@ -248,64 +159,234 @@ else { Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!; if (Homeserver is null) return; Room = Homeserver.GetRoom(RoomId!); + IsInitialised = true; + StateHasChanged(); await Task.WhenAll( Task.Run(async () => { PowerLevels = (await Room.GetPowerLevelsAsync())!; }), - Task.Run(async () => { DraupnirShortcode = (await Room.GetStateOrNullAsync(MjolnirShortcodeEventContent.EventId))?.Shortcode; }), - Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }), - Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); }), Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync(DraupnirProtectedRoomsData.EventId)) is not null; }) ); StateHasChanged(); - await LoadStatesAsync(); - Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!"); + await LoadStateAsync(firstLoad: true); + Loading = false; + logger.LogInformation($"Policy list editor initialized in {sw.Elapsed}!"); } - private async Task LoadStatesAsync() { + private async Task LoadStateAsync(bool firstLoad = false) { + 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}"); + + 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() 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($"{proxySafeProps?.Count} proxy safe props found in {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 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 (!mappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + + var collection = PolicyCollections[mappedType]; + + var key = (evt.Type, evt.StateKey!); + var policyInfo = new PolicyCollection.PolicyInfo { + Policy = evt, + MadeRedundantBy = [] + }; + if (evt.RawContent is null or { Count: 0 } || string.IsNullOrWhiteSpace(evt.RawContent?["recommendation"]?.GetValue())) { + collection.ActivePolicies.Remove(key); + if (!collection.RemovedPolicies.TryAdd(key, policyInfo)) { + if (StateEvent.Equals(collection.RemovedPolicies[key].Policy, evt)) continue; + collection.RemovedPolicies[key] = policyInfo; + } + } + else { + collection.RemovedPolicies.Remove(key); + if (!collection.ActivePolicies.TryAdd(key, policyInfo)) { + if (StateEvent.Equals(collection.ActivePolicies[key].Policy, evt)) continue; + collection.ActivePolicies[key] = policyInfo; + } + } + } + + logger.LogInformation($"LoadStatesAsync: Processed state in {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("LoadStatesAsync: Scanning for redundant policies..."); + + Loading = false; + var allPolicies = PolicyCollections.Values + .SelectMany(x => x.ActivePolicies.Values) + .Select(x => (x, x.Policy.TypedContent as PolicyRuleEventContent)) + .ToList(); + var wildcardPolicies = allPolicies + .Where(x => x.Item2!.IsGlobRule() || x.Item2 is ServerPolicyRuleEventContent) + .ToList(); + Console.WriteLine($"Got {allPolicies.Count} total policies, {wildcardPolicies.Count} wildcard policies."); + int i = 0; + int hits = 0; + int redundant = 0; + int duplicates = 0; + foreach (var policy in allPolicies) { + if (policy.Item2 is null) continue; + var matchingPolicies = wildcardPolicies + .Where(x => + !StateEvent.TypeKeyPairMatches(policy.x.Policy, x.x.Policy) + && x.Item2!.EntityMatches(policy.Item2.Entity!) + ) + .ToList(); + + if (matchingPolicies.Count > 0) { + logger.LogInformation($"{i} Got {matchingPolicies.Count} hits for {policy.x.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.x.Policy.RawContent).ToJson()}"); + foreach (var match in matchingPolicies) { + policy.x.MadeRedundantBy.Add(match.x.Policy); + } + + hits++; + redundant += matchingPolicies.Count; + + if (hits % 5 == 0) + StateHasChanged(); + } + else logger.LogInformation("Sleeping..."); + await Task.Delay(1); + i++; + } + + i = 0; + foreach (var policy in allPolicies) { + if (policy.Item2 is null) continue; + var matchingPolicies = allPolicies + .Where(x => + !StateEvent.TypeKeyPairMatches(policy.x.Policy, x.x.Policy) + && x.Item2!.Entity == policy.Item2.Entity! + ) + .ToList(); + + if (matchingPolicies.Count > 0) { + logger.LogInformation($"{i} Got {matchingPolicies.Count} duplicates for {policy.x.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.x.Policy.RawContent).ToJson()}"); + foreach (var match in matchingPolicies) { + policy.x.MadeRedundantBy.Add(match.x.Policy); + } + + hits++; + redundant += 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(); + } + + // the old one: + private async Task LoadStatesAsync(bool firstLoad = false) { + await LoadStateAsync(firstLoad); + return; var sw = Stopwatch.StartNew(); Loading = true; // var states = Room.GetFullStateAsync(); var states = await Room.GetFullStateAsListAsync(); // PolicyEventsByType.Clear(); - Console.WriteLine($"LoadStatesAsync: Loaded state in {sw.Elapsed}"); + logger.LogInformation($"LoadStatesAsync: Loaded state in {sw.Elapsed}"); - foreach (var state in states) { - if (state is null) continue; - if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + foreach (var type in KnownPolicyTypes) { + if (!PolicyEventsByType.ContainsKey(type)) + PolicyEventsByType.Add(type, new List + (16000)); + } - if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new()); + int count = 0; + foreach (var state in states) { + var _spsw = Stopwatch.StartNew(); + TimeSpan e1, e2, e3, e4, e5, e6, t; + if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + e1 = _spsw.Elapsed; var targetPolicies = PolicyEventsByType[state.MappedType]; + e2 = _spsw.Elapsed; + if (!firstLoad && targetPolicies.FirstOrDefault(x => StateEvent.TypeKeyPairMatches(x, state)) is { } evt) { + e3 = _spsw.Elapsed; + if (StateEvent.Equals(evt, state)) { + if (count % 100 == 0) { + await Task.Delay(10); + await Task.Yield(); + } - if (targetPolicies.FirstOrDefault(x => StateEvent.TypeKeyPairMatches(x, state)) is { } evt) { - if (StateEvent.Equals(evt, state)) continue; + 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})"); + 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 #{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})"); + } + 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})"); } - else targetPolicies.Add(state); + + // await Task.Delay(10); + // await Task.Yield(); } - Console.WriteLine($"LoadStatesAsync: Processed state in {sw.Elapsed}"); + logger.LogInformation($"LoadStatesAsync: Processed state in {sw.Elapsed}"); Loading = false; StateHasChanged(); await Task.Delay(10); await Task.Yield(); - Console.WriteLine($"LoadStatesAsync: yield finished in {sw.Elapsed}"); + logger.LogInformation($"LoadStatesAsync: yield finished in {sw.Elapsed}"); } - // private List AllPolicies { get; set; } = []; - private List GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; - private List GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue())).ToList(); - - private List GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => x.RawContent is { Count: > 0 } && string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue())).ToList(); - - private List GetRemovedPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => x.RawContent is null or { Count: 0 }).ToList(); + // private List GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue())).ToList(); + // + // private List GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => x.RawContent is { Count: > 0 } && string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue())).ToList(); + // + // private List GetRemovedPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => x.RawContent is null or { Count: 0 }).ToList(); private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() ?? type.GetCustomAttributes() @@ -313,23 +394,6 @@ 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(); - } - - private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { - await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent); - CurrentlyEditingEvent = null; - await LoadStatesAsync(); - } - - private async Task UpgradePolicyAsync(StateEventResponse policyEvent) { - policyEvent.RawContent["gay.rory.matrixutils.upgraded_from_type"] = policyEvent.Type; - await LoadStatesAsync(); - } - private static FrozenSet KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); // event types, unnamed @@ -339,154 +403,28 @@ else { private static Dictionary PolicyTypeIds = KnownPolicyTypes .ToDictionary(x => x, x => x.GetCustomAttributes().Select(y => y.EventName).ToArray()); - 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 struct PolicyStats { - public int Active { get; set; } - public int Invalid { get; set; } - public int Removed { get; set; } - } - -#region Draupnir interop - - private SemaphoreSlim ss = new(16, 16); - - private async Task DraupnirKickMatching(StateEventResponse policy) { - try { - var content = policy.TypedContent! as PolicyRuleEventContent; - if (content is null) return; - if (string.IsNullOrWhiteSpace(content.Entity)) return; - - var data = await Homeserver.GetAccountDataAsync(DraupnirProtectedRoomsData.EventId); - var rooms = data.Rooms.Select(Homeserver.GetRoom).ToList(); + Dictionary PolicyCollections { get; set; } = new(); - 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(); - // // 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(); - // 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); - } - } + public struct PolicyCollection { + public required string Name { get; init; } + public int TotalCount => ActivePolicies.Count + RemovedPolicies.Count; -#region Nasty, nasty internals, please ignore! + public required Dictionary<(string Type, string StateKey), PolicyInfo> ActivePolicies { get; set; } - private static class NastyInternalsPleaseIgnore { - public static async Task ExecuteKickWithWasmWorkers(WebWorkerService workerService, AuthenticatedHomeserverGeneric hs, StateEventResponse evt, List 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(); - // 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); - } - } + // public Dictionary<(string Type, string StateKey), StateEventResponse> InvalidPolicies { get; set; } + public required Dictionary<(string Type, string StateKey), PolicyInfo> RemovedPolicies { get; set; } + public required FrozenDictionary PropertiesToDisplay { get; set; } - private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, StateEventResponse policy) { - Console.WriteLine($"Checking {roomId}..."); - Console.WriteLine(policy.EventId); + public struct PolicyInfo { + public required StateEventResponse Policy { get; init; } + public required List MadeRedundantBy { get; set; } } } -#endregion - -#endregion + // private struct PolicyStats { + // public int Active { get; set; } + // public int Invalid { get; set; } + // public int Removed { get; set; } + // } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs new file mode 100644 index 0000000..0106c6e --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs @@ -0,0 +1,144 @@ +using LibMatrix; +using LibMatrix.EventTypes.Interop.Draupnir; +using LibMatrix.EventTypes.Spec.State.Policy; +using LibMatrix.Homeservers; +using LibMatrix.Services; +using SpawnDev.BlazorJS.WebWorkers; + +namespace MatrixUtils.Web.Pages.Rooms; + +public partial class PolicyList { + +#region Draupnir interop + + private SemaphoreSlim ss = new(16, 16); + + private async Task DraupnirKickMatching(StateEventResponse policy) { + try { + var content = policy.TypedContent! as PolicyRuleEventContent; + if (content is null) return; + if (string.IsNullOrWhiteSpace(content.Entity)) return; + + var data = await Homeserver.GetAccountDataAsync(DraupnirProtectedRoomsData.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(); + // // 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(); + // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue; + // if (membership?.Membership is "leave" or "ban") continue; + // + // if (content.EntityMatches(member.StateKey!)) + // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); + // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); + // } + // ActiveKicks[policy]--; + // StateHasChanged(); + // } + // finally { + // Console.WriteLine($"Finished checking {room.RoomId}..."); + // } + // } + // }); + } + finally { + ActiveKicks.Remove(policy); + StateHasChanged(); + await Task.Delay(500); + } + } + +#region Nasty, nasty internals, please ignore! + + private static class NastyInternalsPleaseIgnore { + public static async Task ExecuteKickWithWasmWorkers(WebWorkerService workerService, AuthenticatedHomeserverGeneric hs, StateEventResponse evt, List 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(); + // if (member.StateKey == hs.WhoAmI.UserId) continue; + // if (membership?.Membership is "leave" or "ban") continue; + // + // if (entity == member.StateKey) + // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); + // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); + // } + } + catch (Exception e) { + Console.WriteLine(e); + } + } + + private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, StateEventResponse policy) { + Console.WriteLine($"Checking {roomId}..."); + Console.WriteLine(policy.EventId); + } + } + +#endregion + +#endregion + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css deleted file mode 100644 index afe9fb0..0000000 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css +++ /dev/null @@ -1,9 +0,0 @@ -th { - border-width: 1px; -} - -table { - width: fit-content; - border-width: 1px; - vertical-align: middle; -} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor index 982fc5a..5d5bb5d 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor @@ -15,7 +15,7 @@

Policy list editor - Editing @RoomId


@* *@ -Create new policy +Create new policy @if (Loading) {

Loading...

@@ -71,16 +71,16 @@ else { }
@if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) { - Edit - Remove + Edit + Remove @if (policy.IsLegacyType) { - Update policy type + Update policy type } @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.EventId)) { - Make permanent (wildcard) + Make permanent (wildcard) @if (CurrentUserIsDraupnir) { - Kick matching users + Kick matching users } } else { diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor new file mode 100644 index 0000000..f818b62 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor @@ -0,0 +1,66 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.RoomTypes +
+ + + @($"{PolicyCollection.Name}: {PolicyCollection.TotalCount} policies") + +
+
+ + + + @foreach (var name in PolicyCollection.PropertiesToDisplay!.Keys) { + + } + + + + + @foreach (var policy in PolicyCollection.ActivePolicies.Values.OrderBy(x => x.Policy.RawContent?["entity"]?.GetValue())) { + + } + +
@nameActions
+
+ + + @("Invalid " + PolicyCollection.Name.ToLower()) + + + + + + + + + + + @foreach (var policy in PolicyCollection.RemovedPolicies.Values) { + + + + + } + +
State keyJson contents
@policy.Policy.StateKey +
@policy.Policy.RawContent.ToJson(true, false)
+
+
+
+ +@code { + + [Parameter] + public required PolicyList.PolicyCollection PolicyCollection { get; set; } + + [Parameter] + public required GenericRoom Room { get; set; } + + 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..e82f17d --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor @@ -0,0 +1,65 @@ +@using LibMatrix +@using LibMatrix.EventTypes.Common +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Shared.PolicyEditorComponents +

Policy list editor - Editing @(RoomName ?? Room.RoomId)

+@if (!string.IsNullOrWhiteSpace(DraupnirShortcode)) { + Shortcode: @DraupnirShortcode +} +@if (!string.IsNullOrWhiteSpace(RoomAlias)) { + Alias: @RoomAlias +} +
+@* *@ +Create new policy + +Create many new policies + +Refresh + +@if (CurrentlyEditingEvent is not null) { + +} + +@if (MassCreatePolicies) { + +} + +@code { + [Parameter] + public required GenericRoom Room { get; set; } + + [Parameter] + public required Func ReloadStateAsync { get; set; } + + private string? RoomName { get; set; } + private string? RoomAlias { get; set; } + private string? DraupnirShortcode { get; set; } + + private StateEventResponse? CurrentlyEditingEvent { get; set { field = value; StateHasChanged(); } } + private bool MassCreatePolicies { get; set { field = value; StateHasChanged(); } } + + protected override async Task OnInitializedAsync() { + await Task.WhenAll( + Task.Run(async () => { DraupnirShortcode = (await Room.GetStateOrNullAsync(MjolnirShortcodeEventContent.EventId))?.Shortcode; }), + Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }), + Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); }) + ); + + StateHasChanged(); + } + + private async Task UpdatePolicyAsync(StateEventResponse evt) { + Console.WriteLine("UpdatePolicyAsync in PolicyListEditorHeader not yet implementeD!"); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor new file mode 100644 index 0000000..11de82c --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor @@ -0,0 +1,167 @@ +@using System.Reflection +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.Policy +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Shared.PolicyEditorComponents + +@if (_isInitialized && IsVisible) { + + @foreach (var prop in PolicyCollection.PropertiesToDisplay.Values) { + if (prop.Name == "Entity") { + + @TruncateMxid(TypedContent.Entity) + @foreach (var dup in PolicyInfo.MadeRedundantBy) { +
+ Also matched by @dup.FriendlyTypeName.ToLower() @TruncateMxid(dup.RawContent["entity"].GetValue()) + } + + } + else { + @prop.GetGetMethod()?.Invoke(TypedContent, null) + } + } + +
+ @* @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, Policy.Type)) { *@ + @if (true) { + Edit + + Remove + @if (Policy.IsLegacyType) { + Update policy type + } + + @if (TypedContent.Entity?.StartsWith("@*:", StringComparison.Ordinal) == true) { + Convert to ACL + } + + @* @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(Policy.Type)) { *@ + @* Make permanent *@ + @* *@ + @* @if (CurrentUserIsDraupnir) { *@ + @* Kick *@ + @* users @(ActiveKicks.TryGetValue(Policy, out var kick) ? $"({kick})" : null) *@ + @* *@ + @* } *@ + // } + } + else { +

No permission to modify

+ } +
+ + + + @if (IsEditing) { + + } + @* TODO: Implement ability to turn ACLs into wildcards *@ + @*@if (ServerPolicyToMakePermanent is not null) { + + + + }*@ +} + + + +@code { + + [Parameter] + public PolicyList.PolicyCollection.PolicyInfo PolicyInfo { get; set; } + + [Parameter] + public GenericRoom Room { get; set; } = null!; + + [Parameter] + public required PolicyList.PolicyCollection PolicyCollection { get; set; } + + private StateEventResponse Policy => PolicyInfo.Policy; + + private bool IsEditing { + get; + set { + field = value; + _isDirty = true; + StateHasChanged(); + } + } + + public bool IsVisible { + get; + set { + field = value; + _isDirty = true; + } + } = true; + + private PolicyRuleEventContent TypedContent { get; set; } + + private bool _isDirty = true; + private bool _isInitialized; + + protected override bool ShouldRender() => _isDirty; + + protected override void OnParametersSet() { + TypedContent = Policy.TypedContent as PolicyRuleEventContent ?? throw new InvalidOperationException("Policy must have a typed content of type PolicyRuleEventContent."); + _isDirty = true; + _isInitialized = true; + // Console.WriteLine($"ParametersSet {Policy.StateKey}"); + } + + private static string TruncateMxid(string? mxid) { + if (string.IsNullOrWhiteSpace(mxid)) return mxid; + var parts = mxid.Split(':', 2); + if (parts[0].Length > 50) + parts[0] = parts[0][..50] + "[...]"; + + if (parts is [_, { Length: > 50 }]) + parts[1] = parts[1][..50] + "[...]"; + + return parts.Length == 1 ? parts[0] : $"{parts[0]}:{parts[1]}"; + } + + private async Task RemovePolicyAsync() { + await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, new { }); + IsVisible = false; + StateHasChanged(); + // PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); + // await LoadStatesAsync(); + } + + private async Task UpdatePolicyAsync(StateEventResponse evt) { + await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, Policy.RawContent); + // CurrentlyEditingEvent = null; + // await LoadStatesAsync(); + } + + private async Task UpgradePolicyAsync() { + Policy.RawContent["gay.rory.matrixutils.upgraded_from_type"] = Policy.Type; + // await LoadStatesAsync(); + } + + private async Task ConvertToAclAsync() { + if (Policy.RawContent.ContainsKey("entity")) { + var newContent = Policy.ContentAs(); + newContent!.Entity = newContent.Entity!.Replace("@*:", ""); + await Room.SendStateEventAsync(ServerPolicyRuleEventContent.EventId, newContent.GetDraupnir2StateKey(), newContent); + await Room.SendStateEventAsync(Policy.Type, Policy.StateKey!, new { }); + IsVisible = false; + StateHasChanged(); + } + else { + throw new InvalidOperationException("Policy event must contain an 'entity' field to convert to ACL."); + } + } + + private string Anchor(string anchor) { + return $"{NavigationManager.Uri.Split('#')[0]}#{anchor}"; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor index 2903ab8..6df56ba 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor @@ -1,12 +1,24 @@ @page "/PolicyLists" +@using ArcaneLibs @using ArcaneLibs.Extensions @using LibMatrix @using LibMatrix.EventTypes @using LibMatrix.EventTypes.Common @using LibMatrix.EventTypes.Spec.State.Policy +@using LibMatrix.Helpers +@using LibMatrix.Responses @using LibMatrix.RoomTypes @inject ILogger logger -

Policy lists

@* Create new policy list *@ +

+ Policy lists + + + +

+ @if (!string.IsNullOrWhiteSpace(Status)) {

@Status

@@ -16,22 +28,17 @@ }
- +
- + @foreach (var room in Rooms.OrderByDescending(x => x.PolicyCounts.Sum(y => y.Value))) { - + }
Room name Policies
- - - - @room.RoomName @if (room.IsLegacy) { @@ -50,11 +57,44 @@ @(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Server) ?? 0) server policies
@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Room) ?? 0) room policies
+ + + +
+@if (ShowPolicyListCreationWindow && Homeserver != null) { + + @if (!string.IsNullOrWhiteSpace(_roomBuilder.Avatar.Url)) { + + } + else { + + } +
+ +
+ # + + :@Homeserver!.ServerName +
+ + +
+
+ + Bot shortcode: + +
+ Create + +
+} + @code { private List Rooms { get; } = []; @@ -65,44 +105,39 @@ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; + isLoading = true; Status = "Fetching rooms..."; - - var userEventTypes = EventContent.GetMatchingEventTypes(); - var serverEventTypes = EventContent.GetMatchingEventTypes(); - var roomEventTypes = EventContent.GetMatchingEventTypes(); - var knownPolicyTypes = (List) [..userEventTypes, ..serverEventTypes, ..roomEventTypes]; - - List roomsByType = []; + List _tasks = []; await foreach (var room in Homeserver.GetJoinedRoomsByType("support.feline.policy.lists.msc.v1")) { - roomsByType.Add(room); + // roomsByType.Add(room); Status2 = $"Found {room.RoomId} (MSC3784)..."; + _tasks.Add(Task.Run(async () => { + Rooms.Add(await RoomInfo.FromRoom(room)); + StateHasChanged(); + })); } - List> tasks = roomsByType.Select(async room => { - Status2 = $"Fetching room {room.RoomId}..."; - return await RoomInfo.FromRoom(room); - }).ToList(); + await Task.WhenAll(_tasks); - var results = tasks.ToAsyncEnumerable(); - await foreach (var result in results) { - Rooms.Add(result); - StateHasChanged(); - } + isLoading = false; + Status = ""; + Status2 = ""; + } + private async Task ScanLegacyLists() { + isLoading = true; Status = "Searching for legacy lists..."; - var rooms = (await Homeserver.GetJoinedRooms()) .Where(x => !Rooms.Any(y => y.Room.RoomId == x.RoomId)) .Select(async room => { var state = await room.GetFullStateAsListAsync(); var policies = state - .Where(x => knownPolicyTypes.Contains(x.Type)) + .Where(x => PolicyRoom.SpecPolicyEventTypes.Contains(x.Type)) .ToList(); if (policies.Count == 0) return null; Status2 = $"Found legacy list {room.RoomId}..."; return await RoomInfo.FromRoom(room, state, true); - }) - .ToAsyncEnumerable(); + }).ToAsyncEnumerable(); await foreach (var room in rooms) { if (room is not null) { @@ -110,32 +145,36 @@ StateHasChanged(); } } - + + isLoading = false; Status = ""; Status2 = ""; - await base.OnInitializedAsync(); } - private string _status; - - public string Status { - get => _status; + private string? Status { + get; set { - _status = value; + field = value; StateHasChanged(); } } - private string _status2; - - public string Status2 { - get => _status2; + private string? Status2 { + get; set { - _status2 = value; + field = value; StateHasChanged(); } } + private bool ShowPolicyListCreationWindow { + get; + set { + field = value; + StateHasChanged(); + } + } = true; + private class RoomInfo { public GenericRoom Room { get; set; } public string RoomName { get; set; } @@ -149,11 +188,6 @@ Server } - private static readonly List userEventTypes = EventContent.GetMatchingEventTypes(); - private static readonly List serverEventTypes = EventContent.GetMatchingEventTypes(); - private static readonly List roomEventTypes = EventContent.GetMatchingEventTypes(); - private static readonly List allKnownPolicyTypes = [..userEventTypes, ..serverEventTypes, ..roomEventTypes]; - public static async Task FromRoom(GenericRoom room, List? state = null, bool legacy = false) { state ??= await room.GetFullStateAsListAsync(); return new RoomInfo() { @@ -165,12 +199,40 @@ ?? room.RoomId, Shortcode = (await room.GetStateOrNullAsync(MjolnirShortcodeEventContent.EventId))?.Shortcode, PolicyCounts = new() { - { PolicyType.User, state.Count(x => userEventTypes.Contains(x.Type)) }, - { PolicyType.Server, state.Count(x => serverEventTypes.Contains(x.Type)) }, - { PolicyType.Room, state.Count(x => roomEventTypes.Contains(x.Type)) } + { PolicyType.User, state.Count(x => PolicyRoom.UserPolicyEventTypes.Contains(x.Type)) }, + { PolicyType.Server, state.Count(x => PolicyRoom.ServerPolicyEventTypes.Contains(x.Type)) }, + { PolicyType.Room, state.Count(x => PolicyRoom.RoomPolicyEventTypes.Contains(x.Type)) } } }; } } + private readonly RoomBuilder _roomBuilder = new() { + Type = "support.feline.policy.lists.msc.v1", + Name = new() { Name = "New policy list" }, + AliasLocalPart = "policies" + }; + + private readonly MjolnirShortcodeEventContent _shortcodeEvent = new() { + Shortcode = "policy-list" + }; + + private bool isLoading = true; + + private static readonly SvgIdenticonGenerator IdenticonGenerator = new(); + + private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) { + var res = await Homeserver!.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType); + Console.WriteLine(res); + _roomBuilder.Avatar.Url = res; + StateHasChanged(); + } + + private async Task CreatePolicyList() { + var room = await _roomBuilder.Create(Homeserver!); + Status = $"Created policy list {room.RoomId} ({room.GetNameAsync()})"; + await room.SendStateEventAsync(MjolnirShortcodeEventContent.EventId, _shortcodeEvent); + NavigationManager.NavigateTo($"/Rooms/{room.RoomId}/Policies"); + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css deleted file mode 100644 index f9b5b3f..0000000 --- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css +++ /dev/null @@ -1,6 +0,0 @@ -table, th, td { - border-width: 1px; -} -td { - padding: 8px; -} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor new file mode 100644 index 0000000..c1ee202 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor @@ -0,0 +1,52 @@ +@using ArcaneLibs +@using LibMatrix.Helpers + + Room name: + + + + + + Room alias: + + + + + + Room icon: + + @if (!string.IsNullOrWhiteSpace(roomBuilder.Avatar.Url)) { + + } + else { + + } +
+ +
+ +
+ + + +@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 + + Room type: + + @if (RoomTypes.ContainsKey(roomBuilder.Type ?? "")) { + + @foreach (var type in RoomTypes) { + + } + + + } + else { + + } + + version + @if (Capabilities is null) { + Loading... + } + else { + + @foreach (var version in Capabilities.Capabilities.RoomVersions!.Available!) { + + } + + } + + + + Allow attribution: + + + Allow attribution to Rory&::MatrixUtils + ? + + + +@if (ShowAttributionInfo) { + + This will add the following to the room creation content: +
+
{ "gay.rory.created_using": "Rory&::MatrixUtils (https://mru.rory.gay)" }
+ This is not visible to users unless they manually inspect the room's create event source. +
+} + +@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("rmu.room_create.allow_attribution") ?? true; + } + + private static Dictionary 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..272bd8b --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor @@ -0,0 +1,52 @@ +@using System.Text.Json +@using LibMatrix +@using LibMatrix.Helpers + + Initial room state: + + @foreach (var (displayName, events) in new Dictionary>() { + { "Important room state (before final access rules)", roomBuilder.ImportantState }, + { "Additional room state (after final access rules)", roomBuilder.InitialState }, + }) { +
+ + @code + { + // private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" }; + } + + @* @displayName: @events.Count(x => !ImplementedStates.Contains(x.Type)) events *@ + @displayName: @events.Count events + + @* @foreach (var initialState in events.Where(x => !ImplementedStates.Contains(x.Type))) { *@ + @foreach (var initialState in events) { + + + + + } +
+ @(initialState.Type): + @if (!string.IsNullOrEmpty(initialState.StateKey)) { +
+ (@initialState.StateKey) + } + +
+
@JsonSerializer.Serialize(initialState.RawContent, new JsonSerializerOptions { WriteIndented = true })
+
+
+ } + + + +@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/RoomCreateMembershipOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor new file mode 100644 index 0000000..5170d2d --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor @@ -0,0 +1,55 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.Helpers + + Invited members: + +
+ @roomBuilder.Invites.Count members + Invite all logged in accounts + @foreach (var member in roomBuilder.Invites) { + + : + + } +
+ + + + Banned members: + +
+ @roomBuilder.Bans.Count members + @foreach (var member in roomBuilder.Bans) { + + : + + } +
+ + + +@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/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 + + Permissions: +
+ + @if (roomBuilder.Version is "org.matrix.hydra.11" or "12") { + @(roomBuilder.AdditionalCreators.Count + 1) creators, + } + @roomBuilder.PowerLevels.Users.Count members, @roomBuilder.PowerLevels.Events.Count events + + + @if (roomBuilder.Version is "org.matrix.hydra.11" or "12") { + Creators: +
+ @Homeserver.WhoAmI.UserId (you - to change, visit the homepage.) +
+ + +
+ } + + Events:
+ @foreach (var eventType in roomBuilder.PowerLevels.Events.Keys) { + var _event = eventType; + + + - + +
+ + + : +
+ + + + + + } + + + + + + + + + Users:
+ @foreach (var user in roomBuilder.PowerLevels.Users.Keys) { + var _user = user; + + + - + +
+ + + : +
+ + + + + + } + + + + + + + +
+ + + +@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 + + Join rules: + + + + + + + + + + + + History visibility: + + + + + + + + + + + Guest access: + + + Allow guests to join + ? + + + + Server ACLs: + + @if (roomBuilder.ServerAcls?.Allow is null) { +

No allow rules exist!

+ Create sane defaults + } + else { +
+ @(roomBuilder.ServerAcls.Allow?.Count) allow rules + +
+ } + @if (roomBuilder.ServerAcls?.Deny is null) { +

No deny rules exist!

+ Create sane defaults + } + else { +
+ @(roomBuilder.ServerAcls.Deny?.Count) deny rules + +
+ } + + + +@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/RoomCreateUpgradeOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor new file mode 100644 index 0000000..3e8c3dd --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor @@ -0,0 +1,33 @@ +@using LibMatrix.Helpers + + Room upgrade options + +
+ Upgrading from @roomUpgrade.OldRoom.RoomId + + Invite members +
+ + Invite users with powerlevels +
+ + Copy bans (do not use with moderation bots!) +
+ Apply + +
+ + + +@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/Timeline.razor b/MatrixUtils.Web/Pages/Rooms/Timeline.razor index 108581c..2af819b 100644 --- a/MatrixUtils.Web/Pages/Rooms/Timeline.razor +++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor @@ -3,6 +3,7 @@ @using LibMatrix @using LibMatrix.EventTypes.Spec @using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Responses

RoomManagerTimeline


Loaded @Events.Count events...

-- cgit 1.5.1