diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index b7ebae2..9c35673 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -1,7 +1,6 @@
@page "/Rooms/{RoomId}/Policies"
@using LibMatrix
@using ArcaneLibs.Extensions
-@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.EventTypes.Spec.State.Policy
@using System.Diagnostics
@using LibMatrix.RoomTypes
@@ -9,13 +8,25 @@
@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
+@inject WebWorkerService WebWorkerService
-<h3>Policy list editor - Editing @RoomId</h3>
+<h3>Policy list editor - Editing @(RoomName ?? RoomId)</h3>
+@if (!string.IsNullOrWhiteSpace(DraupnirShortcode)) {
+ <span style="margin-right: 2em;">Shortcode: @DraupnirShortcode</span>
+}
+@if (!string.IsNullOrWhiteSpace(RoomAlias)) {
+ <span>Alias: @RoomAlias</span>
+}
<hr/>
@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@
<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton>
+<LinkButton OnClick="@(() => { MassCreatePolicies = true; return Task.CompletedTask; })">Create many new policies</LinkButton>
@if (Loading) {
<p>Loading...</p>
@@ -24,6 +35,8 @@ else if (PolicyEventsByType is not { Count: > 0 }) {
<p>No policies yet</p>
}
else {
+ var renderSw = Stopwatch.StartNew();
+ var renderTotalSw = Stopwatch.StartNew();
@foreach (var (type, value) in PolicyEventsByType) {
<p>
@(GetValidPolicyEventsByType(type).Count) active,
@@ -33,6 +46,8 @@ else {
</p>
}
+ Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}");
+
@foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) {
<details>
<summary>
@@ -41,7 +56,7 @@ else {
</span>
<hr style="margin: revert;"/>
</summary>
- <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
+ <table class="table table-striped table-hover">
@{
var policies = GetValidPolicyEventsByType(type);
var invalidPolicies = GetInvalidPolicyEventsByType(type);
@@ -51,13 +66,18 @@ else {
.Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
.ToFrozenSet();
var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet();
+
+ var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => props.Any(y => y.Name == x.Name))
+ .ToFrozenSet();
+ Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
}
<thead>
<tr>
@foreach (var name in propNames) {
- <th style="border-width: 1px">@name</th>
+ <th>@name</th>
}
- <th style="border-width: 1px">Actions</th>
+ <th>Actions</th>
</tr>
</thead>
<tbody style="border-width: 1px;">
@@ -65,10 +85,6 @@ else {
<tr>
@{
var typedContent = policy.TypedContent!;
- var proxySafeProps = typedContent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
- .Where(x => props.Any(y => y.Name == x.Name))
- .ToFrozenSet();
- Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
}
@foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) {
<td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td>
@@ -81,6 +97,16 @@ else {
@if (policy.IsLegacyType) {
<LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
}
+
+ @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.Type)) {
+ <LinkButton OnClick="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent</LinkButton>
+ @if (CurrentUserIsDraupnir) {
+ <LinkButton Color="@(ActiveKicks.ContainsKey(policy) ? "#FF0000" : null)" OnClick="@(() => DraupnirKickMatching(policy))">Kick users @(ActiveKicks.ContainsKey(policy) ? $"({ActiveKicks[policy]})" : null)</LinkButton>
+ }
+ }
+ }
+ else {
+ <p>No permission to modify</p>
}
</div>
</td>
@@ -94,11 +120,11 @@ else {
@("Invalid " + GetPolicyTypeName(type).ToLower())
</u>
</summary>
- <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
+ <table class="table table-striped table-hover">
<thead>
<tr>
- <th style="border-width: 1px">State key</th>
- <th style="border-width: 1px">Json contents</th>
+ <th>State key</th>
+ <th>Json contents</th>
</tr>
</thead>
<tbody>
@@ -115,12 +141,25 @@ else {
</details>
</details>
}
+
+ Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
+ Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}");
}
@if (CurrentlyEditingEvent is not null) {
<PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal>
}
+@if (ServerPolicyToMakePermanent is not null) {
+ <ModalWindow Title="Make policy permanent">
+
+ </ModalWindow>
+}
+
+@if (MassCreatePolicies) {
+ <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => { MassCreatePolicies = false; LoadStatesAsync(); })"></MassPolicyEditorModal>
+}
+
@code {
#if DEBUG
@@ -130,21 +169,15 @@ else {
#endif
private bool Loading { get; set; } = true;
- //get room list
- // - sync withroom list filter
- // Type = support.feline.msc3784
- //support.feline.policy.lists.msc.v1
[Parameter]
- public string RoomId { get; set; } = null!;
+ public string RoomId { get; set; }
private bool _enableAvatars;
private StateEventResponse? _currentlyEditingEvent;
+ private bool _massCreatePolicies;
+ private StateEventResponse? _serverPolicyToMakePermanent;
- // static readonly Dictionary<string, string?> Avatars = new();
- // static readonly Dictionary<string, RemoteHomeserver> Servers = new();
-
- // private static List<StateEventResponse> PolicyEvents { get; set; } = new();
private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
private StateEventResponse? CurrentlyEditingEvent {
@@ -155,25 +188,44 @@ else {
}
}
- // public bool EnableAvatars {
- // get => _enableAvatars;
- // set {
- // _enableAvatars = value;
- // if (value) GetAllAvatars();
- // }
- // }
+ public StateEventResponse? ServerPolicyToMakePermanent {
+ get => _serverPolicyToMakePermanent;
+ set {
+ _serverPolicyToMakePermanent = value;
+ StateHasChanged();
+ }
+ }
private AuthenticatedHomeserverGeneric Homeserver { get; set; }
private GenericRoom Room { get; set; }
private RoomPowerLevelEventContent PowerLevels { get; set; }
+ public bool CurrentUserIsDraupnir { get; set; }
+ public string? RoomName { get; set; }
+ public string? RoomAlias { get; set; }
+ public string? DraupnirShortcode { get; set; }
+ public Dictionary<StateEventResponse, int> ActiveKicks { get; set; } = [];
+
+ public bool MassCreatePolicies {
+ get => _massCreatePolicies;
+ set {
+ _massCreatePolicies = value;
+ StateHasChanged();
+ }
+ }
protected override async Task OnInitializedAsync() {
var sw = Stopwatch.StartNew();
await base.OnInitializedAsync();
- Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!;
+ Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!;
if (Homeserver is null) return;
Room = Homeserver.GetRoom(RoomId!);
- PowerLevels = (await Room.GetPowerLevelsAsync())!;
+ await Task.WhenAll([
+ Task.Run(async () => { PowerLevels = (await Room.GetPowerLevelsAsync())!; }),
+ Task.Run(async () => { DraupnirShortcode = (await Room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode; }),
+ Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }),
+ Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); }),
+ Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>("org.matrix.mjolnir.protected_rooms")) is not null; }),
+ ]);
await LoadStatesAsync();
Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
}
@@ -193,48 +245,13 @@ else {
StateHasChanged();
}
- // private async Task GetAllAvatars() {
- // // if (!_enableAvatars) return;
- // Console.WriteLine("Getting avatars...");
- // var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue<string>()).Where(x => x.Contains(':') && !x.Contains("*")).ToList();
- // Console.WriteLine($"Got {users.Count} users!");
- // var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList());
- // Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!");
- // var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable();
- // await foreach (var server in homeserverTasks) {
- // if (server is null) continue;
- // var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList();
- // await Task.WhenAll(profileTasks);
- // profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } });
- // foreach (var profile in profileTasks.Select(x => x.Result!.Value)) {
- // // if (profile is null) continue;
- // if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) {
- // var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl);
- // Avatars.TryAdd(profile.Key, url);
- // }
- // else Avatars.TryAdd(profile.Key, null);
- // }
- //
- // StateHasChanged();
- // }
- // }
- //
- // private async Task<KeyValuePair<string, UserProfileResponse>?> TryGetProfile(RemoteHomeserver server, string mxid) {
- // try {
- // return new KeyValuePair<string, UserProfileResponse>(mxid, await server.GetProfileAsync(mxid));
- // }
- // catch {
- // return null;
- // }
- // }
-
private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
- .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+ .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
- .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+ .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
?? type.GetCustomAttributes<MatrixEventAttribute>()
@@ -265,4 +282,139 @@ else {
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());
+
+#region Draupnir interop
+
+ private SemaphoreSlim ss = new(16, 16);
+
+ private async Task DraupnirKickMatching(StateEventResponse policy) {
+ try {
+ var content = policy.TypedContent! as PolicyRuleEventContent;
+ if (content is null) return;
+ if (string.IsNullOrWhiteSpace(content.Entity)) return;
+
+ var data = await Homeserver.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId);
+ var rooms = data.Rooms.Select(Homeserver.GetRoom).ToList();
+
+ ActiveKicks.Add(policy, rooms.Count);
+ StateHasChanged();
+ await Task.Delay(500);
+
+ // for (int i = 0; i < 12; i++) {
+ // _ = WebWorkerService.TaskPool.Invoke(WasteCpu);
+ // }
+
+ // static async Task runKicks(string roomId, PolicyRuleEventContent content) {
+ // Console.WriteLine($"Checking {roomId}...");
+ // // Console.WriteLine($"Checking {room.RoomId}...");
+ // //
+ // // try {
+ // // var members = await room.GetMembersListAsync();
+ // // foreach (var member in members) {
+ // // var membership = member.ContentAs<RoomMemberEventContent>();
+ // // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue;
+ // // if (membership?.Membership is "leave" or "ban") continue;
+ // //
+ // // if (content.EntityMatches(member.StateKey!))
+ // // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given");
+ // // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)");
+ // // }
+ // // }
+ // // finally {
+ // // Console.WriteLine($"Finished checking {room.RoomId}...");
+ // // }
+ // }
+ //
+ // try {
+ // var tasks = rooms.Select(room => WebWorkerService.TaskPool.Invoke(runKicks, room.RoomId, content)).ToList();
+ //
+ // await Task.WhenAll(tasks);
+ // }
+ // catch (Exception e) {
+ // Console.WriteLine(e);
+ // }
+
+ await NastyInternalsPleaseIgnore.ExecuteKickWithWasmWorkers(WebWorkerService, Homeserver, policy, data.Rooms);
+ // await Task.Run(async () => {
+ // foreach (var room in rooms) {
+ // try {
+ // Console.WriteLine($"Checking {room.RoomId}...");
+ // var members = await room.GetMembersListAsync();
+ // foreach (var member in members) {
+ // var membership = member.ContentAs<RoomMemberEventContent>();
+ // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue;
+ // if (membership?.Membership is "leave" or "ban") continue;
+ //
+ // if (content.EntityMatches(member.StateKey!))
+ // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given");
+ // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)");
+ // }
+ // ActiveKicks[policy]--;
+ // StateHasChanged();
+ // }
+ // finally {
+ // Console.WriteLine($"Finished checking {room.RoomId}...");
+ // }
+ // }
+ // });
+ }
+ finally {
+ ActiveKicks.Remove(policy);
+ StateHasChanged();
+ await Task.Delay(500);
+ }
+ }
+
+#region Nasty, nasty internals, please ignore!
+
+ private static class NastyInternalsPleaseIgnore {
+ public static async Task ExecuteKickWithWasmWorkers(WebWorkerService workerService, AuthenticatedHomeserverGeneric hs, StateEventResponse evt, List<string> roomIds) {
+ try {
+ // var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal, hs.WellKnownUris.Client, hs.AccessToken, roomId, content.Entity)).ToList();
+ var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal2, hs.WellKnownUris, hs.AccessToken, roomId, evt)).ToList();
+ // workerService.TaskPool.Invoke(ExecuteKickInternal, hs.BaseUrl, hs.AccessToken, roomIds, content.Entity);
+ await Task.WhenAll(tasks);
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ }
+ }
+
+ private static async Task ExecuteKickInternal(string homeserverBaseUrl, string accessToken, string roomId, string entity) {
+ try {
+ Console.WriteLine("args: " + string.Join(", ", homeserverBaseUrl, accessToken, roomId, entity));
+ Console.WriteLine($"Checking {roomId}...");
+ var hs = new AuthenticatedHomeserverGeneric(homeserverBaseUrl, new() { Client = homeserverBaseUrl }, null, accessToken);
+ Console.WriteLine($"Got HS...");
+ var room = hs.GetRoom(roomId);
+ Console.WriteLine($"Got room...");
+ var members = await room.GetMembersListAsync();
+ Console.WriteLine($"Got members...");
+ // foreach (var member in members) {
+ // var membership = member.ContentAs<RoomMemberEventContent>();
+ // if (member.StateKey == hs.WhoAmI.UserId) continue;
+ // if (membership?.Membership is "leave" or "ban") continue;
+ //
+ // if (entity == member.StateKey)
+ // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given");
+ // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)");
+ // }
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ }
+ }
+
+ private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, StateEventResponse policy) {
+ Console.WriteLine($"Checking {roomId}...");
+ Console.WriteLine(policy.EventId);
+ }
+ }
+
+#endregion
+
+#endregion
+
}
\ No newline at end of file
|