MatrixRoomUtils

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | LICENSE

commit a357bec1831611758a19bf23ff0fa5a5fe99ca52
parent ac7ed016b00941380099d9a0b2601f4bc353f39f
Author: TheArcaneBrony <myrainbowdash949@gmail.com>
Date:   Sun, 28 May 2023 11:30:53 +0200

Add changes

Diffstat:
MMatrixRoomUtils.Core/AuthenticatedHomeServer.cs | 21++++++++++++++-------
AMatrixRoomUtils.Core/Extensions/DictionaryExtensions.cs | 16++++++++++++++++
MMatrixRoomUtils.Core/Interfaces/IHomeServer.cs | 2+-
MMatrixRoomUtils.Core/Responses/CreateRoomRequest.cs | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
MMatrixRoomUtils.Core/StateEvent.cs | 36++++++++++++++++++++----------------
AMatrixRoomUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AMatrixRoomUtils.Web/Classes/RoomCreationTemplates/IRoomCreationTemplate.cs | 10++++++++++
MMatrixRoomUtils.Web/Pages/LoginPage.razor | 41++++++-----------------------------------
MMatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor | 495+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
AMatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor | 42++++++++++++++++++++++++++++++++++++++++++
AMatrixRoomUtils.Web/Shared/SimpleComponents/FancyTextBox.razor | 36++++++++++++++++++++++++++++++++++++
AMatrixRoomUtils.Web/Shared/SimpleComponents/StringListEditor.razor | 32++++++++++++++++++++++++++++++++
AMatrixRoomUtils.Web/Shared/SimpleComponents/ToggleSlider.razor | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AMatrixRoomUtils.Web/Shared/UserListItem.razor | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MMatrixRoomUtils.Web/wwwroot/index.html | 2+-
15 files changed, 810 insertions(+), 284 deletions(-)

diff --git a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs @@ -1,7 +1,9 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; +using System.Text.Json.Nodes; using MatrixRoomUtils.Core.Interfaces; +using MatrixRoomUtils.Core.Responses; namespace MatrixRoomUtils.Core; @@ -72,6 +74,17 @@ public class AuthenticatedHomeServer : IHomeServer } + public async Task<Room> CreateRoom(CreateRoomRequest creationEvent) + { + var res = await _httpClient.PostAsJsonAsync("/_matrix/client/r0/createRoom", creationEvent); + if (!res.IsSuccessStatusCode) + { + Console.WriteLine($"Failed to create room: {await res.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to create room: {await res.Content.ReadAsStringAsync()}"); + } + + return await GetRoom((await res.Content.ReadFromJsonAsync<JsonObject>())!["room_id"]!.ToString()!); + } @@ -83,12 +96,6 @@ public class AuthenticatedHomeServer : IHomeServer { _authenticatedHomeServer = authenticatedHomeServer; } - - - - - - - } + } diff --git a/MatrixRoomUtils.Core/Extensions/DictionaryExtensions.cs b/MatrixRoomUtils.Core/Extensions/DictionaryExtensions.cs @@ -0,0 +1,15 @@ +namespace MatrixRoomUtils.Core.Extensions; + +public static class DictionaryExtensions +{ + public static bool ChangeKey<TKey, TValue>(this IDictionary<TKey, TValue> dict, + TKey oldKey, TKey newKey) + { + TValue value; + if (!dict.Remove(oldKey, out value)) + return false; + + dict[newKey] = value; // or dict.Add(newKey, value) depending on ur comfort + return true; + } +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs @@ -93,7 +93,7 @@ public class IHomeServer { while (_profileCache[mxid] == null) { - Console.WriteLine($"Waiting for profile cache for {mxid}, currently {_profileCache[mxid]?.ToJson()} within {_profileCache.Count} profiles..."); + Console.WriteLine($"Waiting for profile cache for {mxid}, currently {_profileCache[mxid]?.ToJson() ?? "null"} within {_profileCache.Count} profiles..."); await Task.Delay(Random.Shared.Next(50, 500)); } return _profileCache[mxid]; diff --git a/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs b/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.RegularExpressions; +using MatrixRoomUtils.Core.Extensions; namespace MatrixRoomUtils.Core.Responses; @@ -26,6 +27,22 @@ public class CreateRoomRequest /// For use only when you can't use the CreationContent property /// </summary> + public StateEvent this[string event_type, string event_key = ""] + { + get => InitialState.First(x => x.Type == event_type && x.StateKey == event_key); + set + { + var stateEvent = InitialState.FirstOrDefault(x => x.Type == event_type && x.StateKey == event_key); + if (stateEvent == null) + { + InitialState.Add(value); + } + else + { + InitialState[InitialState.IndexOf(stateEvent)] = value; + } + } + } //extra properties [JsonIgnore] @@ -116,52 +133,118 @@ public class CreateRoomRequest } } - [JsonIgnore] - public string GuestAccess + // [JsonIgnore] + // public string GuestAccess + // { + // get + // { + // var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.guest_access"); + // if (stateEvent == null) + // { + // InitialState.Add(new StateEvent() + // { + // Type = "m.room.guest_access", + // Content = new JsonObject() + // { + // ["guest_access"] = "can_join" + // } + // }); + // return "can_join"; + // } + // + // return stateEvent.ContentAsJsonNode["guest_access"].GetValue<string>(); + // } + // set + // { + // var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.guest_access"); + // if (stateEvent == null) + // { + // InitialState.Add(new StateEvent() + // { + // Type = "m.room.guest_access", + // Content = new JsonObject() + // { + // ["guest_access"] = value + // } + // }); + // } + // else + // { + // var v = stateEvent.ContentAsJsonNode; + // v["guest_access"] = value; + // stateEvent.ContentAsJsonNode = v; + // } + // } + // } + + public ServerACL ServerACLs { get { - var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.guest_access"); + var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.server_acl"); if (stateEvent == null) { InitialState.Add(new StateEvent() { - Type = "m.room.guest_access", + Type = "m.room.server_acl", Content = new JsonObject() { - ["guest_access"] = "can_join" + ["allow"] = new JsonArray() + { + "*" + }, + ["deny"] = new JsonArray() } }); - return "can_join"; + return new ServerACL() + { + Allow = new List<string>() + { + "*" + }, + Deny = new List<string>(), + AllowIpLiterals = true + }; } - - return stateEvent.ContentAsJsonNode["guest_access"].GetValue<string>(); + return new ServerACL() + { + Allow = JsonSerializer.Deserialize<List<string>>(stateEvent.ContentAsJsonNode["allow"]), + Deny = JsonSerializer.Deserialize<List<string>>(stateEvent.ContentAsJsonNode["deny"]), + AllowIpLiterals = true + }; } set { - var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.guest_access"); + Console.WriteLine($"Setting server acl to {value.ToJson()}"); + var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.server_acl"); if (stateEvent == null) { InitialState.Add(new StateEvent() { - Type = "m.room.guest_access", + Type = "m.room.server_acl", Content = new JsonObject() { - ["guest_access"] = value + ["allow"] = JsonArray.Parse(JsonSerializer.Serialize(value.Allow)), + ["deny"] = JsonArray.Parse(JsonSerializer.Serialize(value.Deny)) + ["allow_ip_literals"] = value.AllowIpLiterals } }); } else { var v = stateEvent.ContentAsJsonNode; - v["guest_access"] = value; + v["allow"] = JsonArray.Parse(JsonSerializer.Serialize(value.Allow)); + v["deny"] = JsonArray.Parse(JsonSerializer.Serialize(value.Deny)); + v["allow_ip_literals"] = value.AllowIpLiterals; stateEvent.ContentAsJsonNode = v; + Console.WriteLine($"v={v.ToJson()}"); + Console.WriteLine($"stateEvent.ContentAsJsonNode={stateEvent.ContentAsJsonNode.ToJson()}"); } } } - [JsonIgnore] public CreationContentBaseType _creationContentBaseType; + [JsonIgnore] public CreationContentBaseType _creationContentBaseType; public CreateRoomRequest() => _creationContentBaseType = new(this); @@ -214,4 +297,11 @@ public class PowerLevelEvent public class NotificationsPL { [JsonPropertyName("room")] public int Room { get; set; } = 50; +} + +public class ServerACL +{ + [JsonPropertyName("allow")] public List<string> Allow { get; set; } // = null!; + [JsonPropertyName("deny")] public List<string> Deny { get; set; } // = null!; + [JsonPropertyName("allow_ip_literals")] public bool AllowIpLiterals { get; set; } // = false; } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/StateEvent.cs b/MatrixRoomUtils.Core/StateEvent.cs @@ -6,15 +6,12 @@ namespace MatrixRoomUtils.Core; public class StateEvent { - [JsonPropertyName("content")] - public dynamic Content { get; set; } = new{}; - [JsonPropertyName("state_key")] - public string? StateKey { get; set; } - [JsonPropertyName("type")] - public string Type { get; set; } - [JsonPropertyName("replaces_state")] - public string? ReplacesState { get; set; } - + [JsonPropertyName("content")] public dynamic Content { get; set; } = new { }; + + [JsonPropertyName("state_key")] public string StateKey { get; set; } = ""; + [JsonPropertyName("type")] public string Type { get; set; } + [JsonPropertyName("replaces_state")] public string? ReplacesState { get; set; } + //extra properties [JsonIgnore] public JsonNode ContentAsJsonNode @@ -22,17 +19,24 @@ public class StateEvent get => JsonSerializer.SerializeToNode(Content); set => Content = value; } + + public StateEvent<T> As<T>() where T : class + { + return (StateEvent<T>)this; + } } public class StateEvent<T> : StateEvent where T : class { - public new T content { get; set; } - - - [JsonIgnore] - public new JsonNode ContentAsJsonNode + public StateEvent() { - get => JsonSerializer.SerializeToNode(Content); - set => Content = value.Deserialize<T>(); + //import base content if not an empty object + if (base.Content.GetType() == typeof(T)) + { + Console.WriteLine($"StateEvent<{typeof(T)}> created with base content of type {base.Content.GetType()}. Importing base content."); + Content = base.Content; + } } + [JsonPropertyName("content")] + public T Content { get; set; } } \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs b/MatrixRoomUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs @@ -0,0 +1,110 @@ +using MatrixRoomUtils.Core; +using MatrixRoomUtils.Core.Responses; + +namespace MatrixRoomUtils.Web.Classes.RoomCreationTemplates; + +public class DefaultRoomCreationTemplate : IRoomCreationTemplate +{ + public string Name => "Default"; + public CreateRoomRequest CreateRoomRequest + { + get + { + return new() + { + Name = "My new room", + RoomAliasName = "myroom", + InitialState = new() + { + new() + { + Type = "m.room.history_visibility", + Content = new + { + history_visibility = "world_readable" + } + }, +new StateEvent<Pages.RoomManager.RoomManagerCreateRoom.GuestAccessContent> +{ + Type = "m.room.guest_access", + Content = new() + { + GuestAccess = "can_join" + } +}, + new() + { + Type = "m.room.join_rules", + Content = new + { + join_rule = "public" + } + }, + new() + { + Type = "m.room.server_acl", + Content = new + { + allow = new[] { "*" }, + deny = Array.Empty<string>(), + allow_ip_literals = false + } + }, + new() + { + Type = "m.room.avatar", + Content = new + { + url = "mxc://feline.support/UKNhEyrVsrAbYteVvZloZcFj" + } + } + }, + Visibility = "public", + PowerLevelContentOverride = new() + { + UsersDefault = 0, + EventsDefault = 100, + StateDefault = 50, + Invite = 0, + Redact = 50, + Kick = 50, + Ban = 50, + NotificationsPl = new() + { + Room = 50 + }, + Events = new() + { + { "im.vector.modular.widgets", 50 }, + { "io.element.voice_broadcast_info", 50 }, + { "m.reaction", 100 }, + { "m.room.avatar", 50 }, + { "m.room.canonical_alias", 50 }, + { "m.room.encryption", 100 }, + { "m.room.history_visibility", 100 }, + { "m.room.name", 50 }, + { "m.room.pinned_events", 50 }, + { "m.room.power_levels", 100 }, + { "m.room.redaction", 100 }, + { "m.room.server_acl", 100 }, + { "m.room.tombstone", 100 }, + { "m.room.topic", 50 }, + { "m.space.child", 50 }, + { "org.matrix.msc3401.call", 50 }, + { "org.matrix.msc3401.call.member", 50 } + }, + Users = new() + { + { RuntimeCache.CurrentHomeServer.UserId, 100 }, + }, + }, + CreationContent = new() + { + { + "type", null + } + } + }; + } + } +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Classes/RoomCreationTemplates/IRoomCreationTemplate.cs b/MatrixRoomUtils.Web/Classes/RoomCreationTemplates/IRoomCreationTemplate.cs @@ -0,0 +1,9 @@ +using MatrixRoomUtils.Core.Responses; + +namespace MatrixRoomUtils.Web.Classes.RoomCreationTemplates; + +public interface IRoomCreationTemplate +{ + public CreateRoomRequest CreateRoomRequest { get; } + public string Name { get; } +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/LoginPage.razor b/MatrixRoomUtils.Web/Pages/LoginPage.razor @@ -1,41 +1,21 @@ @page "/Login" @using System.Text.Json @using MatrixRoomUtils.Core.Authentication +@using MatrixRoomUtils.Web.Shared.SimpleComponents @inject ILocalStorageService LocalStorage @inject IJSRuntime JsRuntime <h3>Login</h3> <hr/> <span> - <label>@@</label> - @if (inputVisible.username) - { - <input autofocus @bind="newRecordInput.username" @onfocusout="() => inputVisible.username = false" @ref="elementToFocus"/> - } - else - { - <span tabindex="0" style="border-bottom: #ccc solid 1px; min-width: 50px; display: inline-block; height: 1.4em;" @onfocusin="() => inputVisible.username = true">@newRecordInput.username</span> - } - <label>:</label> - @if (inputVisible.homeserver) - { - <input autofocus @bind="newRecordInput.homeserver" @onfocusout="() => inputVisible.homeserver = false" @ref="elementToFocus"/> - } - else - { - <span tabindex="0" style="border-bottom: #ccc solid 1px; min-width: 50px; display: inline-block; margin-left: 2px; height: 1.4em;" @onfocusin="() => inputVisible.homeserver = true">@newRecordInput.homeserver</span> - } + <span>@@</span><!-- + --><FancyTextBox @bind-Value="@newRecordInput.username"></FancyTextBox><!-- + --><span>:</span><!-- + --><FancyTextBox @bind-Value="@newRecordInput.homeserver"></FancyTextBox> </span> <span style="display: block;"> <label>Password:</label> - @if (inputVisible.password) - { - <input autofocus="true" @bind="newRecordInput.password" @onfocusout="() => inputVisible.password = false" @ref="elementToFocus" type="password"/> - } - else - { - <span tabindex="0" style="border-bottom: #ccc solid 1px; min-width: 50px; display: inline-block; height: 1.4em;" @onfocusin="() => inputVisible.password = true">@string.Join("", newRecordInput.password.Select(x => '*'))</span> - } + <FancyTextBox @bind-Value="@newRecordInput.password" IsPassword="true"></FancyTextBox> </span> <button @onclick="AddRecord">Add account to queue</button> <br/> @@ -63,7 +43,6 @@ @code { List<(string homeserver, string username, string password)> records = new(); (string homeserver, string username, string password) newRecordInput = ("", "", ""); - (bool homeserver, bool username, bool password) inputVisible = (false, false, false); async Task Login() { @@ -106,14 +85,6 @@ } } - - private ElementReference elementToFocus; - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await JsRuntime.InvokeVoidAsync("BlazorFocusElement", elementToFocus); - } - private void AddRecord() { records.Add(newRecordInput); diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor @@ -2,12 +2,21 @@ @using System.Text.Json @using MatrixRoomUtils.Core.Extensions @using MatrixRoomUtils.Core.Responses +@using MatrixRoomUtils.Web.Shared.SimpleComponents +@using System.Reflection @using System.Runtime.Intrinsics.X86 +@using System.Text.Json.Serialization +@using MatrixRoomUtils.Web.Classes.RoomCreationTemplates <h3>Room Manager - Create Room</h3> @* <pre Contenteditable="true" @onkeypress="@JsonChanged" ="JsonString">@JsonString</pre> *@ -<table> - <tr > +<style> + table.table-top-first-tr tr td:first-child { + vertical-align: top; + } +</style> +<table class="table-top-first-tr"> + <tr> <td style="padding-bottom: 16px;">Preset:</td> <td style="padding-bottom: 16px;"> <InputSelect @bind-Value="@RoomPreset"> @@ -15,105 +24,184 @@ { <option value="@createRoomRequest.Key">@createRoomRequest.Key</option> } - @* <option value="private_chat">Private chat</option> *@ - @* <option value="trusted_private_chat">Trusted private chat</option> *@ - @* <option value="public_chat">Public chat</option> *@ - </InputSelect> - </td> - </tr> - <tr> - <td>Room name:</td> - <td> - <InputText @bind-Value="@creationEvent.Name"></InputText> - </td> - </tr> - <tr> - <td>Room alias (localpart):</td> - <td> - <InputText @bind-Value="@creationEvent.RoomAliasName"></InputText> - </td> - </tr> - <tr> - <td>Room type:</td> - <td> - <InputSelect @bind-Value="@creationEvent._creationContentBaseType.Type"> - <option value="">Room</option> - <option value="m.space">Space</option> - </InputSelect> - <InputText @bind-Value="@creationEvent._creationContentBaseType.Type"></InputText> - </td> - </tr> - <tr> - <td style="padding-top: 16px;">History visibility:</td> - <td style="padding-top: 16px;"> - <InputSelect @bind-Value="@creationEvent.HistoryVisibility"> - <option value="invited">Invited</option> - <option value="joined">Joined</option> - <option value="shared">Shared</option> - <option value="world_readable">World readable</option> - </InputSelect> - </td> - </tr> - <tr> - <td>Guest access:</td> - <td> - <InputSelect @bind-Value="@creationEvent.GuestAccess"> - <option value="can_join">Can join</option> - <option value="forbidden">Forbidden</option> </InputSelect> </td> </tr> + @if (creationEvent != null) + { + <tr> + <td>Room name:</td> + <td> + <FancyTextBox @bind-Value="@creationEvent.Name"></FancyTextBox> + </td> + </tr> + <tr> + <td>Room alias (localpart):</td> + <td> + <FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox> + </td> + </tr> + <tr> + <td>Room type:</td> + <td> + <InputSelect @bind-Value="@creationEvent._creationContentBaseType.Type"> + <option value="">Room</option> + <option value="m.space">Space</option> + </InputSelect> + <FancyTextBox @bind-Value="@creationEvent._creationContentBaseType.Type"></FancyTextBox> + </td> + </tr> + <tr> + <td style="padding-top: 16px;">History visibility:</td> + <td style="padding-top: 16px;"> + <InputSelect @bind-Value="@creationEvent.HistoryVisibility"> + <option value="invited">Invited</option> + <option value="joined">Joined</option> + <option value="shared">Shared</option> + <option value="world_readable">World readable</option> + </InputSelect> + </td> + </tr> + <tr> + <td>Guest access:</td> + <td> +@code +{ + bool test { get; set; } = true; + GuestAccessContent a => creationEvent["m.room.guest_access"].As<GuestAccessContent>().Content; +} +<ToggleSlider @bind-Value="a.IsGuestAccessEnabled">@(a.IsGuestAccessEnabled ? "Guests can join" : "Guests cannot join")</ToggleSlider> + @* <InputSelect @bind-Value="@creationEvent.GuestAccess"> *@ + @* <option value="can_join">Can join</option> *@ + @* <option value="forbidden">Forbidden</option> *@ + @* </InputSelect> *@ + </td> + </tr> - <tr> - <td>Room icon:</td> - <td> - <img src="@RuntimeCache.CurrentHomeServer?.ResolveMediaUri(creationEvent.RoomIcon ?? "")" style="max-width: 100px; max-height: 100px; border-radius: 50%;"/> - @* <InputText @bind-Value="@creationEvent.RoomIcon"></InputText> *@ - </td> - - </tr> + <tr> + <td>Room icon:</td> + <td> + <img src="@RuntimeCache.CurrentHomeServer?.ResolveMediaUri(creationEvent.RoomIcon ?? "")" style="width: 128px; height: 128px; border-radius: 50%;"/> + <div style=" display: inline-block; + vertical-align: middle;"> + <FancyTextBox @bind-Value="@creationEvent.RoomIcon"></FancyTextBox><br/> + <InputFile OnChange="RoomIconFilePicked"></InputFile> + </div> - <tr> - <td style="vertical-align: top;">Initial states:</td> - <td> + </td> + </tr> + <tr> + <td>Permissions:</td> <details> - @code{ - - private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", }; - + <summary>@creationEvent.PowerLevelContentOverride.Users.Count members</summary> + @foreach (var user in creationEvent.PowerLevelContentOverride.Events.Keys) + { + var _event = user; + <tr> + <td><FancyTextBox Formatter="@GetPermissionFriendlyName" Value="@_event" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Events.ChangeKey(_event, val); }"></FancyTextBox>:</td> + <td> + <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/> + </td> + </tr> + } + @foreach (var user in creationEvent.PowerLevelContentOverride.Users.Keys) + { + var _user = user; + <tr> + <td><FancyTextBox Value="@_user" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>:</td> + <td> + <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/> + </td> + </tr> } - <summary>@creationEvent.InitialState.Count(x => !ImplementedStates.Contains(x.Type)) custom states</summary> - <table> - @foreach (var initialState in creationEvent.InitialState.Where(x => !ImplementedStates.Contains(x.Type))) - { - <tr> - <td style="vertical-align: top;">@(initialState.Type):</td> - - <td> - <pre>@JsonSerializer.Serialize(initialState.Content, new JsonSerializerOptions { WriteIndented = true })</pre> - </td> - </tr> - } - </table> </details> - <details> - <summary>@creationEvent.InitialState.Count initial states</summary> - <table> - @foreach (var initialState in creationEvent.InitialState.Where(x => !new[] { "m.room.avatar", "m.room.history_visibility" }.Contains(x.Type))) + </tr> + <tr> + <td>Server ACLs:</td> + <td> + <details> + <summary>@(creationEvent.ServerACLs.Allow.Count) allow rules</summary> + <StringListEditor ItemsChanged="OverwriteWrappedProperties" Items="@ServerACLAllowRules"></StringListEditor> + </details> + <details> + <summary>@creationEvent.ServerACLs.Deny.Count deny rules</summary> + <StringListEditor ItemsChanged="OverwriteWrappedProperties" Items="@ServerACLDenyRules"></StringListEditor> + </details> + </td> + </tr> + + <tr> + <td>Invited members:</td> + <td> + <details> + <summary>@creationEvent.InitialState.Count(x => x.Type == "m.room.member") members</summary> + <button @onclick="() => { RuntimeCache.LoginSessions.Select(x => x.Value.LoginResponse.UserId).ToList().ForEach(InviteMember); }">Invite all logged in accounts</button> + @foreach (var member in creationEvent.InitialState.Where(x => x.Type == "m.room.member" && x.StateKey != RuntimeCache.CurrentHomeServer.UserId)) { - <tr> - <td style="vertical-align: top;">@(initialState.Type):</td> + <UserListItem UserId="@member.StateKey"></UserListItem> + } + </details> + </td> + </tr> + + @* Initial states, should remain at bottom? *@ + + <tr> + <td style="vertical-align: top;">Initial states:</td> + <td> + <details> + + @code{ + + private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" }; - <td> - <pre>@JsonSerializer.Serialize(initialState.Content, new JsonSerializerOptions { WriteIndented = true })</pre> - </td> - </tr> } - </table> - </details> - </td> - </tr> + + <summary>@creationEvent.InitialState.Count(x => !ImplementedStates.Contains(x.Type)) custom states</summary> + <table> + @foreach (var initialState in creationEvent.InitialState.Where(x => !ImplementedStates.Contains(x.Type))) + { + <tr> + <td style="vertical-align: top;"> + @(initialState.Type): + @if (!string.IsNullOrEmpty(initialState.StateKey)) + { + <br/> + <span>(@initialState.StateKey)</span> + } + </td> + + <td> + <pre>@JsonSerializer.Serialize(initialState.Content, new JsonSerializerOptions { WriteIndented = true })</pre> + </td> + </tr> + } + </table> + </details> + <details> + <summary>@creationEvent.InitialState.Count initial states</summary> + <table> + @foreach (var initialState in creationEvent.InitialState) + { + var _state = initialState; + <tr> + <td style="vertical-align: top;"> + <span>@(_state.Type):</span><br/> + <button @onclick="() => { creationEvent.InitialState.Remove(_state); StateHasChanged(); }">Remove</button> + </td> + + <td> + <pre>@JsonSerializer.Serialize(_state.Content, new JsonSerializerOptions { WriteIndented = true })</pre> + </td> + </tr> + } + </table> + </details> + </td> + </tr> + } </table> +<button @onclick="CreateRoom">Create room</button> <br/> <details> <summary>Creation JSON</summary> @@ -123,22 +211,14 @@ </details> <details open> <summary>Creation JSON (with null values)</summary> - <EditablePre @bind-Value="@JsonString" oninput="@JsonChanged"></EditablePre> + <pre> + @creationEvent.ToJson() + </pre> </details> @code { - private string JsonString - { - get => creationEvent.ToJson(); - set - { - creationEvent = JsonSerializer.Deserialize<CreateRoomRequest>(value); - JsonChanged(); - } - } - private string RoomPreset { get @@ -153,6 +233,8 @@ { creationEvent = Presets[value]; JsonChanged(); + OverwriteWrappedPropertiesFromEvent(); + creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); } } @@ -166,123 +248,87 @@ { await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); - creationEvent = Presets["Default room"] = new CreateRoomRequest + //creationEvent = Presets["Default room"] = + foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) { - Name = "My new room", - RoomAliasName = "myroom", - InitialState = new() - { - new() - { - Type = "m.room.history_visibility", - Content = new - { - history_visibility = "world_readable" - } - }, - new() - { - Type = "m.room.guest_access", - Content = new - { - guest_access = "can_join" - } - }, - new() - { - Type = "m.room.join_rules", - Content = new - { - join_rule = "public" - } - }, - new() - { - Type = "m.room.server_acl", - Content = new - { - allow = new[] { "*" }, - deny = new[] - { - "midov.pl", - "qoto.org", - "matrix.kiwifarms.net", - "plan9.rocks", - "thisisjoes.site", - "konqi.work", - "austinhuang.lol", - "arcticfox.ems.host", - "*.thisisjoes.site", - "*.abuser.eu", - "*.austinhuang.lol" - }, - allow_ip_literals = false - } - }, - new() - { - Type = "m.room.avatar", - Content = new - { - url = "mxc://feline.support/UKNhEyrVsrAbYteVvZloZcFj" - } - } - }, - Visibility = "public", - PowerLevelContentOverride = new() - { - UsersDefault = 0, - EventsDefault = 100, - StateDefault = 50, - Invite = 0, - Redact = 50, - Kick = 50, - Ban = 50, - NotificationsPl = new() - { - Room = 50 - }, - Events = new() - { - { "im.vector.modular.widgets", 50 }, - { "io.element.voice_broadcast_info", 50 }, - { "m.reaction", 100 }, - { "m.room.avatar", 50 }, - { "m.room.canonical_alias", 50 }, - { "m.room.encryption", 100 }, - { "m.room.history_visibility", 100 }, - { "m.room.name", 50 }, - { "m.room.pinned_events", 50 }, - { "m.room.power_levels", 100 }, - { "m.room.redaction", 100 }, - { "m.room.server_acl", 100 }, - { "m.room.tombstone", 100 }, - { "m.room.topic", 50 }, - { "m.space.child", 50 }, - { "org.matrix.msc3401.call", 50 }, - { "org.matrix.msc3401.call.member", 50 } - }, - Users = new() - { - { "@alicia:rory.gay", 100 }, - { "@emma:rory.gay", 100 }, - { "@root:rory.gay", 100 }, - { "@rory:rory.gay", 100 } - }, - }, - CreationContent = new() - { - { "type", null } - } - }; + 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"; await base.OnInitializedAsync(); } private void JsonChanged() { - Console.WriteLine(JsonString); + Console.WriteLine(creationEvent.ToJson()); + } + + + //wrappers + private List<string> ServerACLAllowRules { get; set; } = new(); + private List<string> ServerACLDenyRules { get; set; } = new(); + + private void OverwriteWrappedPropertiesFromEvent() + { + Console.WriteLine("Overwriting wrapped properties from event"); + ServerACLAllowRules = creationEvent.ServerACLs.Allow; + ServerACLDenyRules = creationEvent.ServerACLs.Deny; + } + + private async Task OverwriteWrappedProperties() + { + Console.WriteLine("Overwriting wrapped properties"); + Console.WriteLine($"Allow: {ServerACLAllowRules.Count}: {string.Join(", ", ServerACLAllowRules)}"); + Console.WriteLine($"Deny: {ServerACLDenyRules.Count}: {string.Join(", ", ServerACLDenyRules)}"); + creationEvent.ServerACLs = new() + { + Allow = ServerACLAllowRules, + Deny = ServerACLDenyRules, + AllowIpLiterals = creationEvent.ServerACLs.AllowIpLiterals + }; + + StateHasChanged(); + } + + private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) + { + var res = await RuntimeCache.CurrentHomeServer.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType); + Console.WriteLine(res); + creationEvent.RoomIcon = res; + StateHasChanged(); + } + + private async Task CreateRoom() + { + Console.WriteLine("Create room"); + Console.WriteLine(creationEvent.ToJson()); + creationEvent.CreationContent.Add("rory.gay.created_using", "Rory&::MatrixRoomUtils (https://mru.rory.gay)"); + //creationEvent.CreationContent.Add(); + var id = await RuntimeCache.CurrentHomeServer.CreateRoom(creationEvent); + // NavigationManager.NavigateTo($"/RoomManager/{id.RoomId.Replace('.','~')}"); + } + + private void InviteMember(string mxid) + { + if (!creationEvent.InitialState.Any(x => x.Type == "m.room.member" && x.StateKey == mxid) && RuntimeCache.CurrentHomeServer.UserId != mxid) + creationEvent.InitialState.Add(new() + { + Type = "m.room.member", + StateKey = mxid, + Content = new + { + membership = "invite", + reason = "Automatically invited at room creation time." + } + }); } @@ -295,4 +341,32 @@ _ => key }; -} -\ No newline at end of file + 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", + _ => key + }; + public class GuestAccessContent + { + [JsonPropertyName("guest_access")] + public string GuestAccess { get; set; } + + public bool IsGuestAccessEnabled + { + get => GuestAccess == "can_join"; + set => GuestAccess = value ? "can_join" : "forbidden"; + } + } + + } + + diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor @@ -0,0 +1,41 @@ +@using MatrixRoomUtils.Core.Extensions +<table> + @foreach(var i in Items.Keys) + { + var key = i; + <input value="@Items[key]" @oninput="(obj) => inputChanged(obj, key)"> + <button @onclick="() => { Items.Remove(key); ItemsChanged.InvokeAsync(); }">Remove</button> + <br/> + } +</table> +<button @onclick="() => { Items.Add(string.Empty, default); ItemsChanged.InvokeAsync(); }">Add</button> + +@code { + + [Parameter] + public Dictionary<string, object> Items { get; set; } = new(); + + [Parameter, EditorRequired] + public EventCallback ItemsChanged { get; set; } + + [Parameter] + public Func<string,string>? KeyFormatter { get; set; } + + [Parameter] + public Action? OnFocusLost { get; set; } + + + protected override Task OnInitializedAsync() + { + Console.WriteLine($"DictionaryEditor initialized with {Items.Count} items: {Items.ToJson()}"); + return base.OnInitializedAsync(); + } + + private void inputChanged(ChangeEventArgs obj, string key) + { + Console.WriteLine($"StringListEditor inputChanged {key} {obj.Value}"); + Items[key] = obj.Value.ToString(); + ItemsChanged.InvokeAsync(); + } + +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/FancyTextBox.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/FancyTextBox.razor @@ -0,0 +1,35 @@ +@inject IJSRuntime JsRuntime +@if (isVisible) +{ + <input autofocus @bind="Value" @onfocusout="() => { isVisible = false; ValueChanged.InvokeAsync(Value); }" @ref="elementToFocus"/> +} +else +{ + <span tabindex="0" style="border-bottom: #ccc solid 1px; height: 1.4em; display: inline-block; @(string.IsNullOrEmpty(Value) ? "min-width: 50px;" : "")" @onfocusin="() => isVisible = true">@(Formatter?.Invoke(Value) ?? (IsPassword ? string.Join("", Value.Select(x=>'*')) : Value))</span> +} + +@code { + + [Parameter] + public string Value { get; set; } + + [Parameter] + public bool IsPassword { get; set; } = false; + + [Parameter] + public EventCallback<string> ValueChanged { get; set; } + + [Parameter] + public Func<string?, string>? Formatter { get; set; } + + + private bool isVisible { get; set; } = false; + + private ElementReference elementToFocus; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await JsRuntime.InvokeVoidAsync("BlazorFocusElement", elementToFocus); + } + +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/StringListEditor.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/StringListEditor.razor @@ -0,0 +1,31 @@ +@for (int i = 0; i < Items.Count; i++) +{ + var self = i; + <button @onclick="() => { Items.RemoveAt(self); ItemsChanged.InvokeAsync(); }">Remove</button> + <FancyTextBox Value="@Items[self]" ValueChanged="@(obj => inputChanged(obj, self))"/> + <br/> +} +<button @onclick="() => { Items.Add(string.Empty); ItemsChanged.InvokeAsync(); }">Add</button> + +@code { + + [Parameter] + public List<string> Items { get; set; } = new List<string>(); + + [Parameter, EditorRequired] + public EventCallback ItemsChanged { get; set; } + + protected override Task OnInitializedAsync() + { + Console.WriteLine($"StringListEditor initialized with {Items.Count} items: {string.Join(",", Items)}"); + return base.OnInitializedAsync(); + } + + private void inputChanged(string obj, int i) + { + Console.WriteLine($"StringListEditor inputChanged {i} {obj}"); + Items[i] = obj; + ItemsChanged.InvokeAsync(); + } + +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/ToggleSlider.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/ToggleSlider.razor @@ -0,0 +1,70 @@ +<input type="checkbox"/><span>@ChildContent</span> + +<div class="container"> + <label class="switch" for="checkbox"> + <input type="checkbox" id="checkbox" @bind="Value"/> + <div class="slider round"></div> + </label> +</div> + +<style> + .switch { + display: inline-block; + height: 16px; + position: relative; + width: 32px; + } + + .switch input { + display:none; + } + + .slider { + background-color: #ccc; + bottom: 0; + cursor: pointer; + left: 0; + position: absolute; + right: 0; + top: 0; + transition: .4s; + } + + .slider:before { + background-color: #fff; + bottom: -5px; + content: ""; + height: 26px; + left: -8px; + position: absolute; + transition: .4s; + width: 26px; + } + + input:checked + .slider { + background-color: #66bb6a; + } + + input:checked + .slider:before { + transform: translateX(24px); + } + + .slider.round { + border-radius: 24px; + } + + .slider.round:before { + border-radius: 50%; + } +</style> + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public bool Value { get; set; } + [Parameter] + public EventCallback<bool> ValueChanged { get; set; } + +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Shared/UserListItem.razor b/MatrixRoomUtils.Web/Shared/UserListItem.razor @@ -0,0 +1,62 @@ +@using MatrixRoomUtils.Core.Responses +<div style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-Content;"> + <img style="@(ChildContent != null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%;" src="@profileAvatar"/> + <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px;">@profileName</span> + + <div style="display: inline-block;"> + @if (ChildContent != null) + { + @ChildContent + } + </div> + +</div> + +@code { + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public ProfileResponse User { get; set; } + + [Parameter] + public string UserId { get; set; } + + private string profileAvatar { get; set; } = "/icon-192.png"; + private string profileName { get; set; } = "Loading..."; + + + private static SemaphoreSlim _semaphoreSlim = new(128); + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); + + await _semaphoreSlim.WaitAsync(); + + var hs = await new AuthenticatedHomeServer(RuntimeCache.CurrentHomeServer.UserId, RuntimeCache.CurrentHomeServer.AccessToken, RuntimeCache.CurrentHomeServer.HomeServerDomain).Configure(); + + if (User == null) + { + if (UserId == null) + { + throw new ArgumentNullException(nameof(UserId)); + } + User = await hs.GetProfile(UserId); + } + else + { + // UserId = User.; + } + + profileAvatar = RuntimeCache.CurrentHomeServer.ResolveMediaUri(User.AvatarUrl); + profileName = User.DisplayName; + + _semaphoreSlim.Release(); + if (Random.Shared.Next(100) == 1) + await LocalStorageWrapper.SaveCacheToLocalStorage(LocalStorage); + } + +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/wwwroot/index.html b/MatrixRoomUtils.Web/wwwroot/index.html @@ -32,7 +32,7 @@ if (element instanceof HTMLElement) { console.log(element); element.focus(); - } else { + } else if (element.__internalId) { console.log("Element is not an HTMLElement", element); } }