diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index b7ebae2..4dc2adc 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -16,6 +16,7 @@
<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 +25,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 +36,8 @@ else {
</p>
}
+ Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}");
+
@foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) {
<details>
<summary>
@@ -41,7 +46,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 +56,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 +75,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>
@@ -94,11 +100,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 +121,19 @@ 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 (MassCreatePolicies) {
+ <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => { MassCreatePolicies = false; LoadStatesAsync(); })"></MassPolicyEditorModal>
+}
+
@code {
#if DEBUG
@@ -130,21 +143,14 @@ 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!;
private bool _enableAvatars;
private StateEventResponse? _currentlyEditingEvent;
+ private bool _massCreatePolicies;
- // 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,18 +161,18 @@ else {
}
}
- // public bool EnableAvatars {
- // get => _enableAvatars;
- // set {
- // _enableAvatars = value;
- // if (value) GetAllAvatars();
- // }
- // }
-
private AuthenticatedHomeserverGeneric Homeserver { get; set; }
private GenericRoom Room { get; set; }
private RoomPowerLevelEventContent PowerLevels { get; set; }
+ public bool MassCreatePolicies {
+ get => _massCreatePolicies;
+ set {
+ _massCreatePolicies = value;
+ StateHasChanged();
+ }
+ }
+
protected override async Task OnInitializedAsync() {
var sw = Stopwatch.StartNew();
await base.OnInitializedAsync();
@@ -193,48 +199,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>()
@@ -243,13 +214,13 @@ else {
private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
- await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { });
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { });
PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
await LoadStatesAsync();
}
private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
- await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent);
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent);
CurrentlyEditingEvent = null;
await LoadStatesAsync();
}
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css
new file mode 100644
index 0000000..afe9fb0
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css
@@ -0,0 +1,9 @@
+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
new file mode 100644
index 0000000..adac385
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
@@ -0,0 +1,213 @@
+@page "/Rooms/{RoomId}/Policies2"
+@using LibMatrix
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using System.Diagnostics
+@using LibMatrix.RoomTypes
+@using System.Collections.Frozen
+@using System.Reflection
+@using ArcaneLibs.Attributes
+@using LibMatrix.EventTypes
+
+@using MatrixUtils.Web.Shared.PolicyEditorComponents
+
+<h3>Policy list editor - Editing @RoomId</h3>
+<hr/>
+@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@
+<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton>
+
+@if (Loading) {
+ <p>Loading...</p>
+}
+else if (PolicyEventsByType is not { Count: > 0 }) {
+ <p>No policies yet</p>
+}
+else {
+ var renderSw = Stopwatch.StartNew();
+ var renderTotalSw = Stopwatch.StartNew();
+ @foreach (var (type, value) in PolicyEventsByType) {
+ <p>
+ @(GetValidPolicyEventsByType(type).Count) active,
+ @(GetInvalidPolicyEventsByType(type).Count) invalid
+ (@value.Count total)
+ @(GetPolicyTypeName(type).ToLower())
+ </p>
+ }
+
+ Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}");
+
+ @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) {
+ <details>
+ <summary>
+ <span>
+ @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies")
+ </span>
+ <hr style="margin: revert;"/>
+ </summary>
+ <div class="flex-grid">
+ @{
+ var policies = GetValidPolicyEventsByType(type);
+ var invalidPolicies = GetInvalidPolicyEventsByType(type);
+ // enumerate all properties with friendly name
+ var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null)
+ .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
+ .ToFrozenSet();
+ var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet();
+
+ var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => props.Any(y => y.Name == x.Name))
+ .ToFrozenSet();
+ Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
+ }
+ @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) {
+ <div class="flex-item">
+ @{
+ var typedContent = policy.TypedContent!;
+ }
+ @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) {
+ <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td>
+ }
+ <div style="display: ruby;">
+ @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) {
+ <LinkButton OnClick="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton>
+ <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Remove</LinkButton>
+ @if (policy.IsLegacyType) {
+ <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
+ }
+ }
+ </div>
+ </div>
+ }
+ </div>
+ <details>
+ <summary>
+ <u>
+ @("Invalid " + GetPolicyTypeName(type).ToLower())
+ </u>
+ </summary>
+ <table class="table table-striped table-hover">
+ <thead>
+ <tr>
+ <th>State key</th>
+ <th>Json contents</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var policy in invalidPolicies) {
+ <tr>
+ <td>@policy.StateKey</td>
+ <td>
+ <pre>@policy.RawContent.ToJson(true, false)</pre>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+ </details>
+ }
+
+ Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}");
+ Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}");
+}
+
+@if (CurrentlyEditingEvent is not null) {
+ <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal>
+}
+
+@code {
+
+#if DEBUG
+ private const bool Debug = true;
+#else
+ private const bool Debug = false;
+#endif
+
+ private bool Loading { get; set; } = true;
+
+ [Parameter]
+ public string RoomId { get; set; } = null!;
+
+ private bool _enableAvatars;
+ private StateEventResponse? _currentlyEditingEvent;
+
+ private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
+
+ private StateEventResponse? CurrentlyEditingEvent {
+ get => _currentlyEditingEvent;
+ set {
+ _currentlyEditingEvent = value;
+ StateHasChanged();
+ }
+ }
+
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; }
+ private GenericRoom Room { get; set; }
+ private RoomPowerLevelEventContent PowerLevels { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var sw = Stopwatch.StartNew();
+ await base.OnInitializedAsync();
+ Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!;
+ if (Homeserver is null) return;
+ Room = Homeserver.GetRoom(RoomId!);
+ PowerLevels = (await Room.GetPowerLevelsAsync())!;
+ await LoadStatesAsync();
+ Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
+ }
+
+ private async Task LoadStatesAsync() {
+ Loading = true;
+ var states = Room.GetFullStateAsync();
+ PolicyEventsByType.Clear();
+ await foreach (var state in states) {
+ if (state is null) continue;
+ if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
+ if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new());
+ PolicyEventsByType[state.MappedType].Add(state);
+ }
+
+ Loading = false;
+ StateHasChanged();
+ }
+
+ private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
+
+ private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+
+ private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList();
+
+ private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
+ ?? type.GetCustomAttributes<MatrixEventAttribute>()
+ .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName;
+
+ private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
+
+ private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { });
+ PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
+ await LoadStatesAsync();
+ }
+
+ private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent);
+ CurrentlyEditingEvent = null;
+ await LoadStatesAsync();
+ }
+
+ private async Task UpgradePolicyAsync(StateEventResponse policyEvent) {
+ policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type;
+ await LoadStatesAsync();
+ }
+
+ 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);
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css
new file mode 100644
index 0000000..d224737
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css
@@ -0,0 +1,32 @@
+th {
+ border-width: 1px;
+}
+
+table {
+ width: fit-content;
+ border-width: 1px;
+ vertical-align: middle;
+}
+
+.flex-grid {
+ display: grid;
+ /*grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));*/
+ /*// fit based on content max width*/
+ grid-template-columns: repeat(auto-fill, minmax(min-content, 1fr));
+
+ gap: 10px;
+}
+
+.flex-item {
+ /*flex: 1 1 30%;*/
+ /*margin: 0.25rem;*/
+ /*position: relative;*/
+ /*display: flex;*/
+ /*flex-direction: column;*/
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #fff1;
+ background-clip: border-box;
+ border: 1px solid rgba(0, 0, 0, .125);
+ border-radius: .5rem
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor
index 01ab1c4..8153224 100644
--- a/MatrixUtils.Web/Pages/Rooms/Space.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Space.razor
@@ -1,12 +1,14 @@
@page "/Rooms/{RoomId}/Space"
+@using System.Collections.ObjectModel
@using LibMatrix.RoomTypes
@using ArcaneLibs.Extensions
@using LibMatrix
+@using MatrixUtils.Abstractions
<h3>Room manager - Viewing Space</h3>
<button onclick="@JoinAllRooms">Join all rooms</button>
@foreach (var room in Rooms) {
- <RoomListItem Room="room" ShowOwnProfile="true"></RoomListItem>
+ <RoomListItem RoomInfo="room" ShowOwnProfile="true"></RoomListItem>
}
@@ -27,7 +29,7 @@
private GenericRoom? Room { get; set; }
private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>();
- private List<GenericRoom> Rooms { get; } = new();
+ private List<RoomInfo> Rooms { get; } = new();
private List<string> ServersInSpace { get; } = new();
protected override async Task OnInitializedAsync() {
@@ -43,7 +45,18 @@
var roomId = stateEvent.StateKey;
var room = hs.GetRoom(roomId);
if (room is not null) {
- Rooms.Add(room);
+ Task.Run(async () => {
+ try {
+ Rooms.Add(new(Room, await room.GetFullStateAsListAsync()));
+ }
+ catch (MatrixException e) {
+ if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) {
+ Rooms.Add(new(Room) {
+ RoomName = "M_FORBIDDEN"
+ });
+ }
+ }
+ });
}
break;
}
@@ -96,8 +109,32 @@
// List<Task<RoomIdResponse>> tasks = Rooms.Select(room => room.JoinAsync(ServersInSpace.ToArray())).ToList();
// await Task.WhenAll(tasks);
foreach (var room in Rooms) {
+ await JoinRecursive(room.Room.RoomId);
+ }
+ }
+
+ private async Task JoinRecursive(string roomId) {
+ var room = Room!.Homeserver.GetRoom(roomId);
+ if (room is null) return;
+ try {
await room.JoinAsync(ServersInSpace.ToArray());
+ var joined = false;
+ while (!joined) {
+ var ce = await room.GetCreateEventAsync();
+ if(ce is null) continue;
+ if (ce.Type == "m.space") {
+ var children = room.AsSpace.GetChildrenAsync(true);
+ await foreach (var child in children) {
+ JoinRecursive(child.RoomId);
+ }
+ }
+ joined = true;
+ }
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
}
+
}
}
diff --git a/MatrixUtils.Web/Pages/StreamTest.razor b/MatrixUtils.Web/Pages/StreamTest.razor
index 57d3557..541cfe8 100644
--- a/MatrixUtils.Web/Pages/StreamTest.razor
+++ b/MatrixUtils.Web/Pages/StreamTest.razor
@@ -11,8 +11,8 @@
@* <StreamedImage Stream="@Stream"/> *@
<br/>
- @foreach (var stream in Streams.OrderBy(x=>x.GetHashCode())) {
- <StreamedImage Stream="@stream" style="width: 12em; height: 12em;"/>
+ @foreach (var stream in Streams.OrderBy(x => x.GetHashCode())) {
+ <StreamedImage Stream="@stream" style="width: 12em; height: 12em; object-fit: cover;"/>
}
}
@@ -47,20 +47,34 @@
var roomState = await Homeserver.GetRoom("!dSMpkVKGgQHlgBDSpo:matrix.org").GetFullStateAsListAsync();
var members = roomState.Where(x => x.Type == RoomMemberEventContent.EventId).ToList();
Console.WriteLine($"Got {members.Count()} members");
+ var ss = new SemaphoreSlim(1, 1);
foreach (var stateEventResponse in members) {
// Console.WriteLine(stateEventResponse.ToJson());
var mc = stateEventResponse.TypedContent as RoomMemberEventContent;
if (!string.IsNullOrWhiteSpace(mc?.AvatarUrl)) {
var uri = mc.AvatarUrl[6..].Split('/');
var url = $"/_matrix/media/v3/download/{uri[0]}/{uri[1]}";
+ // Homeserver.GetMediaStreamAsync(mc?.AvatarUrl).ContinueWith(async x => {
+ // await ss.WaitAsync();
+ // var stream = x.Result;
+ // Streams.Add(stream);
+ // StateHasChanged();
+ await Task.Delay(100);
+ // ss.Release();
+ // });
try {
Homeserver.ClientHttpClient.GetStreamAsync(url).ContinueWith(async x => {
+ // await ss.WaitAsync();
var stream = x.Result;
Streams.Add(stream);
StateHasChanged();
+ // await Task.Delay(100);
+ // ss.Release();
});
}
- catch { }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ }
}
}
}
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
index ea1e5f6..a4e3918 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -1,6 +1,7 @@
@page "/Tools/Moderation/MassCMEBan"
@using System.Collections.ObjectModel
@using LibMatrix.EventTypes.Spec.State.Policy
+@using LibMatrix.RoomTypes
<h3>User Trace</h3>
<hr/>
@@ -17,19 +18,19 @@
}
@code {
+
// TODO: Properly implement page to be more useful
private ObservableCollection<string> log { get; set; } = new();
private AuthenticatedHomeserverGeneric hs { get; set; }
-
+
[Parameter, SupplyParameterFromQuery(Name = "room")]
public string roomId { get; set; }
-
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
hs = await RMUStorage.GetCurrentSessionOrNavigate();
if (hs is null) return;
-
+
StateHasChanged();
Console.WriteLine("Rerendered!");
await base.OnInitializedAsync();
@@ -37,33 +38,41 @@
private async Task<string> Execute() {
var room = hs.GetRoom("!fTjMjIzNKEsFlUIiru:neko.dev");
- // var room = hs.GetRoom("!yf7OpOiRDXx6zUGpT6:conduit.rory.gay");
- var users = roomId.Split("\n").Select(x => x.Trim()).Where(x=>x.StartsWith('@')).ToList();
- foreach (var user in users) {
- var exists = false;
- try {
- exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity);
- } catch (Exception e) {
- log.Add($"Failed to get {user}");
- }
+ // var room = hs.GetRoom("!IVSjKMsVbjXsmUTuRR:rory.gay");
+ var users = roomId.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList();
+ var tasks = users.Select(x => ExecuteBan(room, x)).ToList();
+ await Task.WhenAll(tasks);
+
+ StateHasChanged();
+
+ return "";
+ }
- if (!exists) {
+ private async Task ExecuteBan(GenericRoom room, string user) {
+ var exists = false;
+ try {
+ exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity);
+ }
+ catch (Exception e) {
+ log.Add($"Failed to get {user}");
+ }
+
+ if (!exists) {
+ try {
var evt = await room.SendStateEventAsync(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'), new UserPolicyRuleEventContent() {
Entity = user,
- Reason = "spam (invite)",
+ Reason = "spam",
Recommendation = "m.ban"
});
log.Add($"Sent {evt.EventId} to ban {user}");
}
- else {
- log.Add($"User {user} already exists");
+ catch (Exception e) {
+ log.Add($"Failed to ban {user}: {e}");
}
}
-
-
- StateHasChanged();
-
- return "";
+ else {
+ log.Add($"User {user} already exists");
+ }
}
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index e5ba004..94afc9a 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -55,75 +55,92 @@
if (ChronologicalOrder) {
filteredMemberships = filteredMemberships.Reverse();
}
- if(!string.IsNullOrWhiteSpace(Sender)) {
+
+ if (!string.IsNullOrWhiteSpace(Sender)) {
filteredMemberships = filteredMemberships.Where(x => x.Sender == Sender);
}
- if(!string.IsNullOrWhiteSpace(User)) {
+
+ if (!string.IsNullOrWhiteSpace(User)) {
filteredMemberships = filteredMemberships.Where(x => x.StateKey == User);
}
+ <table>
@foreach (var membership in filteredMemberships) {
RoomMemberEventContent content = membership.TypedContent as RoomMemberEventContent;
- @switch (content.Membership) {
- case RoomMemberEventContent.MembershipTypes.Invite: {
- if (_showInvites) {
- <p style="color: green;">@membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
- }
-
- break;
- }
- case RoomMemberEventContent.MembershipTypes.Ban: {
- if (_showBans) {
- <p style="color: red;">@membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
- }
+ StateEventResponse? previous = previousMemberships.GetValueOrDefault(membership.StateKey);
+ RoomMemberEventContent? previousContent = previous?.TypedContent as RoomMemberEventContent;
+ <tr>
+ <td>@DateTimeOffset.FromUnixTimeMilliseconds(membership.OriginServerTs ?? 0).ToString("g")</td>
+ <td>
+ @switch (content.Membership) {
+ case RoomMemberEventContent.MembershipTypes.Invite: {
+ if (_showInvites) {
+ <p style="color: green;">@membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+ }
+
+ break;
+ }
+ case RoomMemberEventContent.MembershipTypes.Ban: {
+ if (_showBans) {
+ <p style="color: red;">@membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+ }
- break;
- }
- case RoomMemberEventContent.MembershipTypes.Leave: {
- if (membership.Sender == membership.StateKey) {
- if (_showLeaves) {
- <p style="color: #C66;">@membership.Sender left the room</p>
+ break;
}
- }
- else {
- if (_showKicks) {
- <p style="color: darkorange;">@membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+ case RoomMemberEventContent.MembershipTypes.Leave: {
+ if (membership.Sender == membership.StateKey) {
+ if (_showLeaves) {
+ <p style="color: #C66;">@membership.Sender left the room</p>
+ }
+ }
+ else {
+ if (_showKicks) {
+ <p style="color: darkorange;">@membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+ }
+ }
+
+ break;
}
- }
-
- break;
- }
- case RoomMemberEventContent.MembershipTypes.Knock: {
- if (_showKnocks) {
- <p>@membership.Sender knocked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
- }
+ case RoomMemberEventContent.MembershipTypes.Knock: {
+ if (_showKnocks) {
+ <p>@membership.Sender knocked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+ }
- break;
- }
- case RoomMemberEventContent.MembershipTypes.Join: {
- if (previousMemberships.TryGetValue(membership.StateKey, out var previous)
- && (previous.TypedContent as RoomMemberEventContent).Membership == RoomMemberEventContent.MembershipTypes.Join) {
- if (_showUpdates) {
- <p style="color: #777;">@membership.Sender changed their profile</p>
+ break;
}
- }
- else {
- if (_showJoins) {
- <p style="color: #6C6;">@membership.Sender joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+ case RoomMemberEventContent.MembershipTypes.Join: {
+ if (previousContent is { Membership: RoomMemberEventContent.MembershipTypes.Join }) {
+ if (_showUpdates) {
+ <p style="color: #777;">
+ @membership.Sender changed their profile<br/>
+ Display name: @previousContent.DisplayName -> @content.DisplayName<br/>
+ Avatar URL: @previousContent.AvatarUrl -> @content.AvatarUrl
+ </p>
+ }
+ }
+ else {
+ if (_showJoins) {
+ <p style="color: #6C6;">
+ @membership.Sender joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")<br/>
+ Display name: @content.DisplayName<br/>
+ Avatar URL: @content.AvatarUrl
+ </p>
+ }
+ }
+
+ break;
+ }
+ default: {
+ <b>Unknown membership @content.Membership!</b>
+ break;
}
}
-
- break;
- }
- default: {
- <b>Unknown membership @content.Membership!</b>
- break;
- }
- }
+ </td>
+ </tr>
previousMemberships[membership.StateKey] = membership;
}
- }
+ </table>}
</details>
<br/>
@@ -217,9 +234,9 @@
StateHasChanged();
}
}
-
+
private string sender = "";
-
+
private string Sender {
get => sender;
set {
@@ -227,9 +244,9 @@
StateHasChanged();
}
}
-
+
private string user = "";
-
+
private string User {
get => user;
set {
diff --git a/MatrixUtils.Web/Shared/MainLayout.razor.css b/MatrixUtils.Web/Shared/MainLayout.razor.css
index 01a5066..924393b 100644
--- a/MatrixUtils.Web/Shared/MainLayout.razor.css
+++ b/MatrixUtils.Web/Shared/MainLayout.razor.css
@@ -57,6 +57,7 @@ main {
.sidebar {
width: 250px;
+ min-width: 250px;
height: 100vh;
position: sticky;
top: 0;
diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
new file mode 100644
index 0000000..11ba18a
--- /dev/null
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
@@ -0,0 +1,102 @@
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using System.Reflection
+@using ArcaneLibs.Attributes
+@using LibMatrix
+@using System.Collections.Frozen
+@using LibMatrix.EventTypes
+@using LibMatrix.RoomTypes
+<ModalWindow Title="@("Creating many new " + (PolicyTypes.ContainsKey(MappedType??"") ? PolicyTypes[MappedType!].GetFriendlyNamePluralOrNull()?.ToLower() ?? PolicyTypes[MappedType!].Name : "event"))"
+ OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="600">
+ <span>Policy type:</span>
+ <select @bind="@MappedType">
+ <option>Select a value</option>
+ @foreach (var (type, mappedType) in PolicyTypes) {
+ <option value="@type">@mappedType.GetFriendlyName().ToLower()</option>
+ }
+ </select><br/>
+
+ <span>Reason:</span>
+ <FancyTextBox @bind-Value="@Reason"></FancyTextBox><br/>
+
+ <span>Recommendation:</span>
+ <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox><br/>
+
+ <span>Entities:</span><br/>
+ <InputTextArea @bind-Value="@Users" style="width: 500px;"></InputTextArea><br/>
+
+
+ @* <details> *@
+ @* <summary>JSON data</summary> *@
+ @* <pre> *@
+ @* $1$ @PolicyEvent.ToJson(true, true) #1# *@
+ @* </pre> *@
+ @* </details> *@
+ <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton>
+ <LinkButton OnClick="@(() => { _ = Save(); return Task.CompletedTask; })"> Save </LinkButton>
+
+</ModalWindow>
+
+@code {
+
+ [Parameter]
+ public required Action OnClose { get; set; }
+
+ [Parameter]
+ public required Action OnSaved { get; set; }
+
+ [Parameter]
+ public required GenericRoom Room { get; set; }
+
+ public string Recommendation { get; set; } = "m.ban";
+ public string Reason { get; set; } = "spam";
+ public string Users { get; set; } = "";
+
+ private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+
+ private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
+ .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+
+ private string? MappedType { get; set; }
+
+ private async Task Save() {
+ try {
+ await DoActualSave();
+ }
+ catch (Exception e) {
+ Console.WriteLine($"Failed to save: {e}");
+ }
+ }
+
+ private async Task DoActualSave() {
+ Console.WriteLine($"Saving ---");
+ Console.WriteLine($"Users = {Users}");
+ var users = Users.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList();
+ var tasks = users.Select(x => ExecuteBan(Room, x)).ToList();
+ await Task.WhenAll(tasks);
+
+ OnSaved.Invoke();
+ }
+
+ private async Task ExecuteBan(GenericRoom room, string entity) {
+ bool success = false;
+ while (!success) {
+ try {
+ var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent;
+ content.Recommendation = Recommendation;
+ content.Reason = Reason;
+ content.Entity = entity;
+ await room.SendStateEventAsync(MappedType!, content.GetDraupnir2StateKey(), content);
+ success = true;
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) throw;
+ Console.WriteLine(e);
+ }
+ catch (Exception e) {
+ //ignored
+ Console.WriteLine(e);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
index 1bd00d1..fc536c0 100644
--- a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
@@ -35,39 +35,61 @@
</thead>
<tbody>
@foreach (var prop in props) {
+ var isNullable = Nullable.GetUnderlyingType(prop.PropertyType) is not null;
<tr>
<td style="padding-right: 8px;">
<span>@prop.GetFriendlyName()</span>
- @if (Nullable.GetUnderlyingType(prop.PropertyType) is not null) {
+ @if (Nullable.GetUnderlyingType(prop.PropertyType) is null) {
<span style="color: red;">*</span>
}
</td>
@{
var getter = prop.GetGetMethod();
var setter = prop.GetSetMethod();
- }
- @switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) {
- case Type t when t == typeof(string):
- <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></FancyTextBox>
- break;
- default:
- <p style="color: red;">Unsupported type: @prop.PropertyType</p>
- break;
+ if (getter is null) {
+ <p style="color: red;">Missing property getter: @prop.Name</p>
+ }
+ else {
+ switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) {
+ case Type t when t == typeof(string):
+ <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></FancyTextBox>
+ break;
+ case Type t when t == typeof(DateTime):
+ if (!isNullable) {
+ <InputDate TValue="DateTime" Value="@(getter?.Invoke(PolicyData, null) as DateTime? ?? new DateTime())" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></InputDate>
+ }
+ else {
+ var value = getter?.Invoke(PolicyData, null) as DateTime?;
+ if (value is null) {
+ <button @onclick="() => { setter?.Invoke(PolicyData, [DateTime.Now]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Add value</button>
+ }
+ else {
+ var notNullValue = Nullable.GetValueRefOrDefaultRef(ref value);
+ Console.WriteLine($"Value: {value?.ToString() ?? "null"}");
+ <InputDate TValue="DateTime" ValueExpression="@(() => notNullValue)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></InputDate>
+ <button @onclick="() => { setter?.Invoke(PolicyData, [null]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Remove value</button>
+ }
+ }
+
+ break;
+ default:
+ <p style="color: red;">Unsupported type: @prop.PropertyType</p>
+ break;
+ }
+ }
}
</tr>
}
</tbody>
</table>
- <br/>
- <pre>
- @PolicyEvent.ToJson(true, false)
- </pre>
+ <details>
+ <summary>JSON data</summary>
+ <pre>
+ @PolicyEvent.ToJson(true, true)
+ </pre>
+ </details>
<LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton>
<LinkButton OnClick="@(() => { OnSave.Invoke(PolicyEvent); return Task.CompletedTask; })"> Save </LinkButton>
- @* <span>Target entity: </span> *@
- @* <FancyTextBox @bind-Value="@policyData.Entity"></FancyTextBox><br/> *@
- @* <span>Reason: </span> *@
- @* <FancyTextBox @bind-Value="@policyData.Reason"></FancyTextBox> *@
}
else {
<p>Policy data is null</p>
@@ -102,7 +124,7 @@
.ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
private StateEventResponse? _policyEvent;
-
+
private string? MappedType {
get => _policyEvent?.Type;
set {
@@ -110,9 +132,9 @@
PolicyEvent.Type = value;
PolicyEvent.TypedContent ??= Activator.CreateInstance(PolicyTypes[value]) as PolicyRuleEventContent;
PolicyData = PolicyEvent.TypedContent as PolicyRuleEventContent;
+ PolicyData.Recommendation ??= "m.ban";
}
}
}
-
}
\ No newline at end of file
|