MatrixRoomUtils

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

commit a67276252c8bfcd6b6c5344e70debc6d67d917a9
parent f143c8cd3adc23a8f4473fc7cea7d1c58322233b
Author: TheArcaneBrony <myrainbowdash949@gmail.com>
Date:   Sat, 27 May 2023 00:39:49 +0200

Been a while since I last committed

Diffstat:
MMatrixRoomUtils.Core/AuthenticatedHomeServer.cs | 12++++++++++++
MMatrixRoomUtils.Core/Authentication/MatrixAuth.cs | 3++-
MMatrixRoomUtils.Core/Interfaces/IHomeServer.cs | 2+-
AMatrixRoomUtils.Core/Responses/CreateRoomRequest.cs | 218+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AMatrixRoomUtils.Core/Responses/StateEventResponse.cs | 35+++++++++++++++++++++++++++++++++++
MMatrixRoomUtils.Core/Room.cs | 34++++++++++++++++++++--------------
MMatrixRoomUtils.Core/RuntimeCache.cs | 36+++++++++++++++++++++---------------
MMatrixRoomUtils.Core/StateEvent.cs | 71++++++++++++++++++++++++++++-------------------------------------------
MMatrixRoomUtils.Web.Server/Pages/Error.cshtml | 2+-
MMatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs | 9+++++++--
AMatrixRoomUtils.Web/FileUploadTest.razor | 20++++++++++++++++++++
MMatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj | 10+---------
MMatrixRoomUtils.Web/Pages/DataExportPage.razor | 43+++++++++++++++++++++----------------------
MMatrixRoomUtils.Web/Pages/DebugTools.razor | 2+-
MMatrixRoomUtils.Web/Pages/DevOptions.razor | 3+--
MMatrixRoomUtils.Web/Pages/Index.razor | 2+-
MMatrixRoomUtils.Web/Pages/KnownHomeserverList.razor | 14+++++++-------
MMatrixRoomUtils.Web/Pages/LoginPage.razor | 119++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
MMatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
MMatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor | 4++--
MMatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor | 46+++++++++++++++++++++++++++++++---------------
AMatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor | 299+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MMatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor | 18+++++++++---------
MMatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor | 40++++++++++++++++++++--------------------
MMatrixRoomUtils.Web/Pages/RoomState/RoomStateRoomList.razor | 2+-
MMatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor | 20++++++++++----------
DMatrixRoomUtils.Web/Pages/UserImportPage.razor | 72------------------------------------------------------------------------
AMatrixRoomUtils.Web/Shared/EditablePre.razor | 19+++++++++++++++++++
MMatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor | 2+-
MMatrixRoomUtils.Web/Shared/MainLayout.razor | 2+-
MMatrixRoomUtils.Web/Shared/RoomListItem.razor | 6+++---
MMatrixRoomUtils.Web/wwwroot/css/app.css | 4++++
MMatrixRoomUtils.Web/wwwroot/index.html | 51+++++++++++++++++++++++++++++++--------------------
33 files changed, 1010 insertions(+), 332 deletions(-)

diff --git a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs @@ -59,6 +59,18 @@ public class AuthenticatedHomeServer : IHomeServer return rooms; } + public async Task<string> UploadFile(string fileName, Stream fileStream, string contentType = "application/octet-stream") + { + var res = await _httpClient.PostAsync($"/_matrix/media/r0/upload?filename={fileName}", new StreamContent(fileStream)); + if (!res.IsSuccessStatusCode) + { + Console.WriteLine($"Failed to upload file: {await res.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to upload file: {await res.Content.ReadAsStringAsync()}"); + } + var resJson = await res.Content.ReadFromJsonAsync<JsonElement>(); + return resJson.GetProperty("content_uri").GetString()!; + } + diff --git a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs b/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs @@ -1,5 +1,6 @@ using System.Net.Http.Json; using System.Text.Json; +using MatrixRoomUtils.Core.Extensions; using MatrixRoomUtils.Core.Responses; namespace MatrixRoomUtils.Core.Authentication; @@ -33,7 +34,7 @@ public class MatrixAuth await Task.Delay(retryAfter.GetInt32()); return await Login(homeserver, username, password); } - + Console.WriteLine($"Login: {data.ToJson()}"); return data.Deserialize<LoginResponse>(); //var token = data.GetProperty("access_token").GetString(); //return token; diff --git a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs @@ -108,7 +108,7 @@ public class IHomeServer _profileCache[mxid] = profile; return profile; } - public async Task<string> ResolveMediaUri(string mxc) + public string ResolveMediaUri(string mxc) { return mxc.Replace("mxc://", $"{FullHomeServerDomain}/_matrix/media/r0/download/"); } diff --git a/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs b/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs @@ -0,0 +1,217 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace MatrixRoomUtils.Core.Responses; + +public class CreateRoomRequest +{ + [JsonPropertyName("name")] public string Name { get; set; } = null!; + + [JsonPropertyName("room_alias_name")] public string RoomAliasName { get; set; } = null!; + + //we dont want to use this, we want more control + // [JsonPropertyName("preset")] + // public string Preset { get; set; } = null!; + [JsonPropertyName("initial_state")] public List<StateEvent> InitialState { get; set; } = null!; + [JsonPropertyName("visibility")] public string Visibility { get; set; } = null!; + + [JsonPropertyName("power_level_content_override")] + public PowerLevelEvent PowerLevelContentOverride { get; set; } = null!; + + [JsonPropertyName("creation_content")] public JsonObject CreationContent { get; set; } = new(); + + /// <summary> + /// For use only when you can't use the CreationContent property + /// </summary> + + + //extra properties + [JsonIgnore] + public string HistoryVisibility + { + get + { + var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.history_visibility"); + if (stateEvent == null) + { + InitialState.Add(new StateEvent() + { + Type = "m.room.history_visibility", + Content = new JsonObject() + { + ["history_visibility"] = "shared" + } + }); + return "shared"; + } + + return stateEvent.ContentAsJsonNode["history_visibility"].GetValue<string>(); + } + set + { + var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.history_visibility"); + if (stateEvent == null) + { + InitialState.Add(new StateEvent() + { + Type = "m.room.history_visibility", + Content = new JsonObject() + { + ["history_visibility"] = value + } + }); + } + else + { + var v = stateEvent.ContentAsJsonNode; + v["history_visibility"] = value; + stateEvent.ContentAsJsonNode = v; + } + } + } + + [JsonIgnore] + public string RoomIcon + { + get + { + var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.avatar"); + if (stateEvent == null) + { + InitialState.Add(new StateEvent() + { + Type = "m.room.avatar", + Content = new JsonObject() + { + ["url"] = "" + } + }); + return ""; + } + + return stateEvent.ContentAsJsonNode["url"].GetValue<string>(); + } + set + { + var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.avatar"); + if (stateEvent == null) + { + InitialState.Add(new StateEvent() + { + Type = "m.room.avatar", + Content = new JsonObject() + { + ["url"] = value + } + }); + } + else + { + var v = stateEvent.ContentAsJsonNode; + v["url"] = value; + stateEvent.ContentAsJsonNode = v; + } + } + } + + [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; + } + } + } + + + [JsonIgnore] public CreationContentBaseType _creationContentBaseType; + + public CreateRoomRequest() => _creationContentBaseType = new(this); + + + public Dictionary<string, string> Validate() + { + Dictionary<string, string> errors = new(); + if (!Regex.IsMatch(RoomAliasName, @"[a-zA-Z0-9_\-]+$")) + errors.Add("room_alias_name", "Room alias name must only contain letters, numbers, underscores, and hyphens."); + + return errors; + } +} + +public class CreationContentBaseType +{ + private readonly CreateRoomRequest createRoomRequest; + + public CreationContentBaseType(CreateRoomRequest createRoomRequest) + { + this.createRoomRequest = createRoomRequest; + } + + [JsonPropertyName("type")] + public string Type + { + get => (string)createRoomRequest.CreationContent["type"]; + set + { + if (value is "null" or "") createRoomRequest.CreationContent.Remove("type"); + else createRoomRequest.CreationContent["type"] = value; + } + } +} + +public class PowerLevelEvent +{ + [JsonPropertyName("ban")] public int Ban { get; set; } // = 50; + [JsonPropertyName("events_default")] public int EventsDefault { get; set; } // = 0; + [JsonPropertyName("events")] public Dictionary<string, int> Events { get; set; } // = null!; + [JsonPropertyName("invite")] public int Invite { get; set; } // = 50; + [JsonPropertyName("kick")] public int Kick { get; set; } // = 50; + [JsonPropertyName("notifications")] public NotificationsPL NotificationsPl { get; set; } // = null!; + [JsonPropertyName("redact")] public int Redact { get; set; } // = 50; + [JsonPropertyName("state_default")] public int StateDefault { get; set; } // = 50; + [JsonPropertyName("users")] public Dictionary<string, int> Users { get; set; } // = null!; + [JsonPropertyName("users_default")] public int UsersDefault { get; set; } // = 0; +} + +public class NotificationsPL +{ + [JsonPropertyName("room")] public int Room { get; set; } = 50; +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Core/Responses/StateEventResponse.cs b/MatrixRoomUtils.Core/Responses/StateEventResponse.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace MatrixRoomUtils.Core; + +public class StateEventResponse +{ + [JsonPropertyName("Content")] + public dynamic Content { get; set; } + [JsonPropertyName("origin_server_ts")] + public long OriginServerTs { get; set; } + [JsonPropertyName("RoomId")] + public string RoomId { get; set; } + [JsonPropertyName("Sender")] + public string Sender { get; set; } + [JsonPropertyName("StateKey")] + public string StateKey { get; set; } + [JsonPropertyName("Type")] + public string Type { get; set; } + [JsonPropertyName("Unsigned")] + public dynamic Unsigned { get; set; } + [JsonPropertyName("EventId")] + public string EventId { get; set; } + [JsonPropertyName("UserId")] + public string UserId { get; set; } + [JsonPropertyName("ReplacesState")] + public string ReplacesState { get; set; } + [JsonPropertyName("PrevContent")] + public dynamic PrevContent { get; set; } +} + +public class StateEventResponse<T> : StateEventResponse where T : class +{ + public T content { get; set; } +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Core/Room.cs b/MatrixRoomUtils.Core/Room.cs @@ -40,14 +40,6 @@ public class Room } var cache = RuntimeCache.GenericResponseCache[cache_key]; - cache.DefaultExpiry = type switch - { - "m.room.name" => TimeSpan.FromMinutes(30), - "org.matrix.mjolnir.shortcode" => TimeSpan.FromHours(4), - "" => TimeSpan.FromSeconds(0), - _ => TimeSpan.FromMinutes(15) - }; - if (cache.ContainsKey(stateCombo)) { if (cache[stateCombo].ExpiryTime > DateTime.Now) @@ -76,14 +68,28 @@ public class Room } var result = await res.Content.ReadFromJsonAsync<JsonElement>(); - - cache[stateCombo] = new GenericResult<object>() + var expiryTime = type switch { - Result = result + "m.room.name" => TimeSpan.FromMinutes(30), + "org.matrix.mjolnir.shortcode" => TimeSpan.FromHours(4), + "" => TimeSpan.FromSeconds(0), + _ => TimeSpan.FromMinutes(15) }; + if(!string.IsNullOrWhiteSpace(type) && !string.IsNullOrWhiteSpace(state_key)) + cache[stateCombo] = new GenericResult<object>() + { + Result = result, + ExpiryTime = DateTime.Now.Add(expiryTime) + }; _semaphore.Release(); return result; } + public async Task<T?> GetStateAsync<T>(string type, string state_key = "", bool logOnFailure = false) + { + var res = await GetStateAsync(type, state_key, logOnFailure); + if (res == null) return default; + return res.Value.Deserialize<T>(); + } public async Task<string> GetNameAsync() { @@ -115,8 +121,8 @@ public class Room var members = new List<string>(); foreach (var member in res.Value.EnumerateArray()) { - if(member.GetProperty("type").GetString() != "m.room.member") continue; - var member_id = member.GetProperty("state_key").GetString(); + if(member.GetProperty("Type").GetString() != "m.room.member") continue; + var member_id = member.GetProperty("StateKey").GetString(); members.Add(member_id); } @@ -196,7 +202,7 @@ public class CreateEvent [JsonPropertyName("room_version")] public string RoomVersion { get; set; } [JsonPropertyName("type")] - public string Type { get; set; } + public string? Type { get; set; } [JsonPropertyName("predecessor")] public object? Predecessor { get; set; } diff --git a/MatrixRoomUtils.Core/RuntimeCache.cs b/MatrixRoomUtils.Core/RuntimeCache.cs @@ -23,6 +23,21 @@ public class RuntimeCache { Console.WriteLine($"RuntimeCache.SaveObject({key}, {value}) was called, but no callback was set!"); }; + + static RuntimeCache() + { + Task.Run(async () => + { + while (true) + { + await Task.Delay(1000); + foreach (var (key, value) in RuntimeCache.GenericResponseCache) + { + SaveObject("rory.matrixroomutils.generic_cache:" + key, value); + } + } + }); + } } @@ -41,7 +56,6 @@ public class HomeServerResolutionResult public class ObjectCache<T> where T : class { public Dictionary<string, GenericResult<T>> Cache { get; set; } = new(); - public TimeSpan DefaultExpiry { get; set; } = new(0, 0, 0); public string Name { get; set; } = null!; public GenericResult<T> this[string key] { @@ -49,19 +63,12 @@ public class ObjectCache<T> where T : class { if (Cache.ContainsKey(key)) { - Console.WriteLine($"cache.get({key}): hit"); + // Console.WriteLine($"cache.get({key}): hit"); // Console.WriteLine($"Found item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}"); - if(Cache[key].ExpiryTime > DateTime.Now) - return Cache[key]; - Console.WriteLine($"Expired item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}"); - try - { - Cache.Remove(key); - } - catch (Exception e) - { - Console.WriteLine($"Failed to remove {key} from cache: {e.Message}"); - } + if(Cache[key].ExpiryTime < DateTime.Now) + Console.WriteLine($"WARNING: item {key} in cache {Name} expired at {Cache[key].ExpiryTime}:\n{Cache[key].Result.ToJson(indent: false)}"); + return Cache[key]; + } Console.WriteLine($"cache.get({key}): miss"); return null; @@ -69,7 +76,6 @@ public class ObjectCache<T> where T : class set { Cache[key] = value; - if(Cache[key].ExpiryTime == null) Cache[key].ExpiryTime = DateTime.Now.Add(DefaultExpiry); Console.WriteLine($"set({key}) = {Cache[key].Result.ToJson(indent:false)}"); Console.WriteLine($"new_state: {this.ToJson(indent:false)}"); // Console.WriteLine($"New item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}"); @@ -90,7 +96,7 @@ public class ObjectCache<T> where T : class // Console.WriteLine($"Removing {x.Key} from cache"); Cache.Remove(x.Key); } - RuntimeCache.SaveObject("rory.matrixroomutils.generic_cache:" + Name, this); + //RuntimeCache.SaveObject("rory.matrixroomutils.generic_cache:" + Name, this); } }); } diff --git a/MatrixRoomUtils.Core/StateEvent.cs b/MatrixRoomUtils.Core/StateEvent.cs @@ -1,53 +1,38 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + namespace MatrixRoomUtils.Core; public class StateEvent { - //example: - /* - { - "content": { - "avatar_url": "mxc://matrix.org/BnmEjNvGAkStmAoUiJtEbycT", - "displayname": "X ⊂ Shekhinah | she/her | you", - "membership": "join" - }, - "origin_server_ts": 1682668449785, - "room_id": "!wDPwzxYCNPTkHGHCFT:the-apothecary.club", - "sender": "@kokern:matrix.org", - "state_key": "@kokern:matrix.org", - "type": "m.room.member", - "unsigned": { - "replaces_state": "$7BWfzN15LN8FFUing1hiUQWFfxnOusrEHYFNiOnNrlM", - "prev_content": { - "avatar_url": "mxc://matrix.org/hEQbGywixsjpxDrWvUYEFNur", - "displayname": "X ⊂ Shekhinah | she/her | you", - "membership": "join" - }, - "prev_sender": "@kokern:matrix.org" - }, - "event_id": "$6AGoMCaxqcOeIIDbez1f0VKwLkOEq3EiVLdlsoxDpNg", - "user_id": "@kokern:matrix.org", - "replaces_state": "$7BWfzN15LN8FFUing1hiUQWFfxnOusrEHYFNiOnNrlM", - "prev_content": { - "avatar_url": "mxc://matrix.org/hEQbGywixsjpxDrWvUYEFNur", - "displayname": "X ⊂ Shekhinah | she/her | you", - "membership": "join" + [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 + { + get => JsonSerializer.SerializeToNode(Content); + set => Content = value; } - } - */ - public dynamic content { get; set; } - public long origin_server_ts { get; set; } - public string room_id { get; set; } - public string sender { get; set; } - public string state_key { get; set; } - public string type { get; set; } - public dynamic unsigned { get; set; } - public string event_id { get; set; } - public string user_id { get; set; } - public string replaces_state { get; set; } - public dynamic prev_content { get; set; } } public class StateEvent<T> : StateEvent where T : class { - public T content { get; set; } + public new T content { get; set; } + + + [JsonIgnore] + public new JsonNode ContentAsJsonNode + { + get => JsonSerializer.SerializeToNode(Content); + set => Content = value.Deserialize<T>(); + } } \ No newline at end of file diff --git a/MatrixRoomUtils.Web.Server/Pages/Error.cshtml b/MatrixRoomUtils.Web.Server/Pages/Error.cshtml @@ -14,7 +14,7 @@ <body> <div class="main"> - <div class="content px-4"> + <div class="Content px-4"> <h1 class="text-danger">Error.</h1> <h2 class="text-danger">An error occurred while processing your request.</h2> diff --git a/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs b/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs @@ -5,6 +5,7 @@ namespace MatrixRoomUtils.Web.Classes; public partial class LocalStorageWrapper { + private static SemaphoreSlim _semaphoreSlim = new(1); public static Settings Settings { get; set; } = new(); //some basic logic @@ -15,6 +16,9 @@ public partial class LocalStorageWrapper } public static async Task LoadFromLocalStorage(ILocalStorageService localStorage) { + await _semaphoreSlim.WaitAsync(); + if (RuntimeCache.WasLoaded) return; + RuntimeCache.WasLoaded = true; Settings = await localStorage.GetItemAsync<Settings>("rory.matrixroomutils.settings") ?? new(); //RuntimeCache stuff @@ -43,7 +47,8 @@ public partial class LocalStorageWrapper Console.WriteLine($"Loading generic cache entry {s}"); RuntimeCache.GenericResponseCache[s.Replace("rory.matrixroomutils.generic_cache:", "")] = await localStorage.GetItemAsync<ObjectCache<object>>(s); } - RuntimeCache.WasLoaded = true; + + _semaphoreSlim.Release(); } public static async Task SaveToLocalStorage(ILocalStorageService localStorage) @@ -70,7 +75,7 @@ public partial class LocalStorageWrapper if (key == "rory.matrixroomutils.user_cache") await localStorage.SetItemAsync(key, RuntimeCache.LoginSessions); if (key == "rory.matrixroomutils.last_used_token") await localStorage.SetItemAsync(key, RuntimeCache.LastUsedToken); if (key == "rory.matrixroomutils.homeserver_resolution_cache") await localStorage.SetItemAsync(key, RuntimeCache.HomeserverResolutionCache); - if (key == "rory.matrixroomutils.generic_cache") await localStorage.SetItemAsync(key, RuntimeCache.GenericResponseCache); + //if (key == "rory.matrixroomutils.generic_cache") await localStorage.SetItemAsync(key, RuntimeCache.GenericResponseCache); } } diff --git a/MatrixRoomUtils.Web/FileUploadTest.razor b/MatrixRoomUtils.Web/FileUploadTest.razor @@ -0,0 +1,19 @@ +@page "/FileUploadTest" +<h3>FileUploadTest</h3> + +<InputFile OnChange="FilePicked" multiple="false"></InputFile> + + +@code { + + private async void FilePicked(InputFileChangeEventArgs obj) + { + Console.WriteLine("FilePicked"); + Console.WriteLine(obj.File.Name); + Console.WriteLine(obj.File.Size); + Console.WriteLine(obj.File.ContentType); + var res = await RuntimeCache.CurrentHomeServer.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType); + Console.WriteLine(res); + } + +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj @@ -15,13 +15,5 @@ <ItemGroup> <ProjectReference Include="..\MatrixRoomUtils.Core\MatrixRoomUtils.Core.csproj" /> </ItemGroup> - - <ItemGroup> - <_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" /> - </ItemGroup> - - <ItemGroup> - <None Include="wwwroot\homeservers.txt" /> - </ItemGroup> - + </Project> diff --git a/MatrixRoomUtils.Web/Pages/DataExportPage.razor b/MatrixRoomUtils.Web/Pages/DataExportPage.razor @@ -15,7 +15,7 @@ { @foreach (var (token, user) in RuntimeCache.LoginSessions) { - <IndexUserItem User="@user"/> + @* <IndexUserItem User="@user"/> *@ <pre> @user.LoginResponse.UserId[1..].Split(":")[0]\auth\access_token=@token @user.LoginResponse.UserId[1..].Split(":")[0]\auth\device_id=@user.LoginResponse.DeviceId @@ -45,32 +45,31 @@ else if (!RuntimeCache.WasLoaded) { await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); - - var homeservers = RuntimeCache.LoginSessions.Values.Select(x => x.LoginResponse.HomeServer).Distinct(); - totalHomeservers = homeservers.Count(); - StateHasChanged(); - foreach (var hs in homeservers) + } + var homeservers = RuntimeCache.LoginSessions.Values.Select(x => x.LoginResponse.HomeServer).Distinct(); + totalHomeservers = homeservers.Count(); + StateHasChanged(); + foreach (var hs in homeservers) + { + if (RuntimeCache.HomeserverResolutionCache.ContainsKey(hs)) { - if (RuntimeCache.HomeserverResolutionCache.ContainsKey(hs)) - { - resolvedHomeservers++; - continue; - } - var resolvedHomeserver = (await new RemoteHomeServer(hs).Configure()).FullHomeServerDomain; - - RuntimeCache.HomeserverResolutionCache.Add(hs, new() { Result = resolvedHomeserver, ResolutionTime = DateTime.Now }); - await LocalStorageWrapper.SaveToLocalStorage(LocalStorage); - - Console.WriteLine("Saved to local storage:"); - Console.WriteLine(JsonSerializer.Serialize(RuntimeCache.HomeserverResolutionCache, new JsonSerializerOptions() - { - WriteIndented = true - })); resolvedHomeservers++; - StateHasChanged(); + continue; } + var resolvedHomeserver = (await new RemoteHomeServer(hs).Configure()).FullHomeServerDomain; + + RuntimeCache.HomeserverResolutionCache.Add(hs, new() { Result = resolvedHomeserver, ResolutionTime = DateTime.Now }); + await LocalStorageWrapper.SaveToLocalStorage(LocalStorage); + + Console.WriteLine("Saved to local storage:"); + Console.WriteLine(JsonSerializer.Serialize(RuntimeCache.HomeserverResolutionCache, new JsonSerializerOptions() + { + WriteIndented = true + })); + resolvedHomeservers++; StateHasChanged(); } + StateHasChanged(); _isLoaded = true; } diff --git a/MatrixRoomUtils.Web/Pages/DebugTools.razor b/MatrixRoomUtils.Web/Pages/DebugTools.razor @@ -40,7 +40,7 @@ else public List<string> Rooms { get; set; } = new(); protected override async Task OnInitializedAsync() { - if (!RuntimeCache.WasLoaded) await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); + await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); await base.OnInitializedAsync(); if (RuntimeCache.CurrentHomeServer == null) { diff --git a/MatrixRoomUtils.Web/Pages/DevOptions.razor b/MatrixRoomUtils.Web/Pages/DevOptions.razor @@ -23,7 +23,6 @@ { <li> @item.Key: @item.Value.Cache.Count entries<br/> - Default expiry: @item.Value.DefaultExpiry<br/> @if (item.Value.Cache.Count > 0) { <p>Earliest expiry: @(item.Value.Cache.Min(x => x.Value.ExpiryTime)) (@string.Format("{0:g}", item.Value.Cache.Min(x => x.Value.ExpiryTime).Value.Subtract(DateTime.Now)) from now)</p> @@ -45,7 +44,7 @@ { while (true) { - await Task.Delay(100); + await Task.Delay(1000); StateHasChanged(); } }); diff --git a/MatrixRoomUtils.Web/Pages/Index.razor b/MatrixRoomUtils.Web/Pages/Index.razor @@ -9,7 +9,7 @@ Small collection of tools to do not-so-everyday things. <br/><br/> -<h5>Signed in accounts - <a href="/Login">Add new account</a> or <a href="/ImportUsers">Import from TSV</a></h5> +<h5>Signed in accounts - <a href="/Login">Add new account</a></h5> <hr/> <form> @foreach (var (token, user) in RuntimeCache.LoginSessions) diff --git a/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor b/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor @@ -84,8 +84,8 @@ else await semaphore.WaitAsync(); progress.ProcessedUsers.Add(room, new()); Console.WriteLine($"Fetching states for room ({rooms.IndexOf(room)}/{rooms.Count}) ({room.RoomId})"); - var states = (await room.GetStateAsync("")).Value.Deserialize<List<StateEvent>>(); - states.RemoveAll(x => x.type != "m.room.member" || x.content.GetProperty("membership").GetString() != "join"); + var states = (await room.GetStateAsync("")).Value.Deserialize<List<StateEventResponse>>(); + states.RemoveAll(x => x.Type != "m.room.member" || x.Content.GetProperty("membership").GetString() != "join"); Console.WriteLine($"Room {room.RoomId} has {states.Count} members"); if (states.Count > memberLimit) { @@ -119,13 +119,13 @@ else { progress.ProcessedUsers[room].Slowmode = false; } - if (!homeServers.Any(x => x.Server == state.state_key.Split(':')[1])) + if (!homeServers.Any(x => x.Server == state.StateKey.Split(':')[1])) { - homeServers.Add(new HomeServerInfo() { Server = state.state_key.Split(':')[1] }); + homeServers.Add(new HomeServerInfo() { Server = state.StateKey.Split(':')[1] }); } - var hs = homeServers.First(x => x.Server == state.state_key.Split(':')[1]); - if (!hs.KnownUsers.Contains(state.state_key.Split(':')[0])) - hs.KnownUsers.Add(state.state_key.Split(':')[0]); + var hs = homeServers.First(x => x.Server == state.StateKey.Split(':')[1]); + if (!hs.KnownUsers.Contains(state.StateKey.Split(':')[0])) + hs.KnownUsers.Add(state.StateKey.Split(':')[0]); if (++progress.ProcessedUsers[room].Processed % updateInterval == 0 && progressCallback != null) { await semLock.WaitAsync(); diff --git a/MatrixRoomUtils.Web/Pages/LoginPage.razor b/MatrixRoomUtils.Web/Pages/LoginPage.razor @@ -1,42 +1,123 @@ @page "/Login" +@using System.Text.Json @using MatrixRoomUtils.Core.Authentication @inject ILocalStorageService LocalStorage +@inject IJSRuntime JsRuntime <h3>Login</h3> +<hr/> -<label>Homeserver:</label> -<input @bind="homeserver"/> -<br/> -<label>Username:</label> -<input @bind="username"/> +<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 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> + } +</span> +<button @onclick="AddRecord">Add account to queue</button> <br/> -<label>Password:</label> -<input @bind="password" type="password"/> + +<InputFile OnChange="@FileChanged" accept=".tsv"></InputFile> <br/> <button @onclick="Login">Login</button> +<br/><br/> +<h4>Parsed records</h4> +<hr/> +<table border="1"> + @foreach (var (homeserver, username, password) in records) + { + <tr style="background-color: @(RuntimeCache.LoginSessions.Any(x => x.Value.LoginResponse.UserId == $"@{username}:{homeserver}") ? "green" : "unset")"> + <td style="border-width: 1px;">@username</td> + <td style="border-width: 1px;">@homeserver</td> + <td style="border-width: 1px;">@password.Length chars</td> + </tr> + } +</table> <br/> <br/> <LogView></LogView> @code { - string homeserver = ""; - string username = ""; - string password = ""; + 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() { - var result = await MatrixAuth.Login(homeserver, username, password); - Console.WriteLine($"Obtained access token for {result.UserId}!"); + foreach (var (homeserver, username, password) in records) + { + if (RuntimeCache.LoginSessions.Any(x => x.Value.LoginResponse.UserId == $"@{username}:{homeserver}")) continue; + var result = await MatrixAuth.Login(homeserver, username, password); + Console.WriteLine($"Obtained access token for {result.UserId}!"); - RuntimeCache.LastUsedToken = result.AccessToken; + var userinfo = new UserInfo() + { + LoginResponse = result + }; + userinfo.Profile = await (await new AuthenticatedHomeServer(result.UserId, result.AccessToken, result.HomeServer).Configure()).GetProfile(result.UserId); + RuntimeCache.LastUsedToken = result.AccessToken; - var userinfo = new UserInfo() - { - LoginResponse = result, - Profile = await (await new RemoteHomeServer(result.HomeServer).Configure()).GetProfile(result.UserId) - }; - RuntimeCache.LoginSessions.Add(userinfo.AccessToken, userinfo); + RuntimeCache.LoginSessions.Add(result.AccessToken, userinfo); + StateHasChanged(); + } await LocalStorageWrapper.SaveToLocalStorage(LocalStorage); } + private async Task FileChanged(InputFileChangeEventArgs obj) + { + Console.WriteLine(JsonSerializer.Serialize(obj, new JsonSerializerOptions() + { + WriteIndented = true + })); + await using var rs = obj.File.OpenReadStream(); + using var sr = new StreamReader(rs); + string TsvData = await sr.ReadToEndAsync(); + records.Clear(); + foreach (var line in TsvData.Split('\n')) + { + var parts = line.Split('\t'); + if (parts.Length != 3) + continue; + records.Add((parts[0], parts[1], parts[2])); + } + } + + + private ElementReference elementToFocus; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await JsRuntime.InvokeVoidAsync("BlazorFocusElement", elementToFocus); + } + + private void AddRecord() + { + records.Add(newRecordInput); + newRecordInput = ("", "", ""); + } + } \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor @@ -8,13 +8,14 @@ <hr/> <p> - This policy list contains @PolicyEvents.Count(x => x.type == "m.policy.rule.server") server bans, - @PolicyEvents.Count(x => x.type == "m.policy.rule.room") room bans and - @PolicyEvents.Count(x => x.type == "m.policy.rule.user") user bans. + This policy list contains @PolicyEvents.Count(x => x.Type == "m.policy.rule.server") server bans, + @PolicyEvents.Count(x => x.Type == "m.policy.rule.room") room bans and + @PolicyEvents.Count(x => x.Type == "m.policy.rule.user") user bans. </p> +<InputCheckbox @bind-Value="_enableAvatars" @oninput="GetAllAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> -@if (!PolicyEvents.Any(x => x.type == "m.policy.rule.server")) +@if (!PolicyEvents.Any(x => x.Type == "m.policy.rule.server")) { <p>No server policies</p> } @@ -22,7 +23,7 @@ else { <h3>Server policies</h3> <hr/> - <table class="table table-striped table-hover" style="width: fit-content;"> + <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> <tr> <th scope="col" style="max-width: 50vw;">Server</th> @@ -32,10 +33,10 @@ else </tr> </thead> <tbody> - @foreach (var policyEvent in PolicyEvents.Where(x => x.type == "m.policy.rule.server" && x.content.Entity != null)) + @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.server" && x.content.Entity != null)) { <tr> - <td>Entity: @policyEvent.content.Entity<br/>State: @policyEvent.state_key</td> + <td>Entity: @policyEvent.content.Entity<br/>State: @policyEvent.StateKey</td> <td>@policyEvent.content.Reason</td> <td> @policyEvent.content.ExpiryDateTime @@ -50,18 +51,18 @@ else </table> <details> <summary>Invalid events</summary> - <table class="table table-striped table-hover" style="width: fit-content;"> + <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> <tr> <th scope="col" style="max-width: 50vw;">State key</th> - <th scope="col">Serialised contents</th> + <th scope="col">Serialised Contents</th> </tr> </thead> <tbody> - @foreach (var policyEvent in PolicyEvents.Where(x => x.type == "m.policy.rule.server" && x.content.Entity == null)) + @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.server" && x.content.Entity == null)) { <tr> - <td>@policyEvent.state_key</td> + <td>@policyEvent.StateKey</td> <td>@policyEvent.content.ToJson(indent: false, ignoreNull: true)</td> </tr> } @@ -69,7 +70,7 @@ else </table> </details> } -@if (!PolicyEvents.Any(x => x.type == "m.policy.rule.room")) +@if (!PolicyEvents.Any(x => x.Type == "m.policy.rule.room")) { <p>No room policies</p> } @@ -77,7 +78,7 @@ else { <h3>Room policies</h3> <hr/> - <table class="table table-striped table-hover" style="width: fit-content;"> + <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> <tr> <th scope="col" style="max-width: 50vw;">Room</th> @@ -87,10 +88,10 @@ else </tr> </thead> <tbody> - @foreach (var policyEvent in PolicyEvents.Where(x => x.type == "m.policy.rule.room" && x.content.Entity != null)) + @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.room" && x.content.Entity != null)) { <tr> - <td>Entity: @policyEvent.content.Entity<br/>State: @policyEvent.state_key</td> + <td>Entity: @policyEvent.content.Entity<br/>State: @policyEvent.StateKey</td> <td>@policyEvent.content.Reason</td> <td> @policyEvent.content.ExpiryDateTime @@ -104,18 +105,18 @@ else </table> <details> <summary>Invalid events</summary> - <table class="table table-striped table-hover" style="width: fit-content;"> + <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> <tr> <th scope="col" style="max-width: 50vw;">State key</th> - <th scope="col">Serialised contents</th> + <th scope="col">Serialised Contents</th> </tr> </thead> <tbody> - @foreach (var policyEvent in PolicyEvents.Where(x => x.type == "m.policy.rule.room" && x.content.Entity == null)) + @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.room" && x.content.Entity == null)) { <tr> - <td>@policyEvent.state_key</td> + <td>@policyEvent.StateKey</td> <td>@policyEvent.content.ToJson(indent: false, ignoreNull: true)</td> </tr> } @@ -123,7 +124,7 @@ else </table> </details> } -@if (!PolicyEvents.Any(x => x.type == "m.policy.rule.user")) +@if (!PolicyEvents.Any(x => x.Type == "m.policy.rule.user")) { <p>No user policies</p> } @@ -131,9 +132,13 @@ else { <h3>User policies</h3> <hr/> - <table class="table table-striped table-hover" style="width: fit-content;"> + <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> <tr> + @if (_enableAvatars) + { + <th scope="col"></th> + } <th scope="col" style="max-width: 0.2vw; word-wrap: anywhere;">User</th> <th scope="col">Reason</th> <th scope="col">Expires</th> @@ -141,10 +146,14 @@ else </tr> </thead> <tbody> - @foreach (var policyEvent in PolicyEvents.Where(x => x.type == "m.policy.rule.user" && x.content.Entity != null)) + @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && x.content.Entity != null)) { <tr> - <td style="word-wrap: anywhere;">Entity: @string.Join("", policyEvent.content.Entity.Take(64))<br/>State: @string.Join("", policyEvent.state_key.Take(64))</td> + @if (_enableAvatars) + { + <td scope="col"><img style="width: 48px; height: 48px; aspect-ratio: unset; border-radius: 50%;" src="@(avatars.ContainsKey(policyEvent.content.Entity) ? avatars[policyEvent.content.Entity] : "")"/></td> + } + <td style="word-wrap: anywhere;">Entity: @string.Join("", policyEvent.content.Entity.Take(64))<br/>State: @string.Join("", policyEvent.StateKey.Take(64))</td> <td>@policyEvent.content.Reason</td> <td> @policyEvent.content.ExpiryDateTime @@ -158,18 +167,18 @@ else </table> <details> <summary>Invalid events</summary> - <table class="table table-striped table-hover" style="width: fit-content;"> + <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> <tr> <th scope="col">State key</th> - <th scope="col">Serialised contents</th> + <th scope="col">Serialised Contents</th> </tr> </thead> <tbody> - @foreach (var policyEvent in PolicyEvents.Where(x => x.type == "m.policy.rule.user" && x.content.Entity == null)) + @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && x.content.Entity == null)) { <tr> - <td>@policyEvent.state_key</td> + <td>@policyEvent.StateKey</td> <td>@policyEvent.content.ToJson(indent: false, ignoreNull: true)</td> </tr> } @@ -183,19 +192,24 @@ else @code { //get room list // - sync withroom list filter - // type = support.feline.msc3784 + // Type = support.feline.msc3784 //support.feline.policy.lists.msc.v1 [Parameter] public string? RoomId { get; set; } + + private bool _enableAvatars = false; + + static Dictionary<string, string> avatars = new Dictionary<string, string>(); + static Dictionary<string, RemoteHomeServer> servers = new Dictionary<string, RemoteHomeServer>(); - public List<StateEvent<PolicyRuleStateEventData>> PolicyEvents { get; set; } = new(); + public static List<StateEventResponse<PolicyRuleStateEventData>> PolicyEvents { get; set; } = new(); protected override async Task OnInitializedAsync() { - if (!RuntimeCache.WasLoaded) await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); + await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); await base.OnInitializedAsync(); - // if(RuntimeCache.AccessToken == null || RuntimeCache.CurrentHomeserver == null) + // if(RuntimeCache.AccessToken == null || RuntimeCache.CurrentHomeserver == null) if (RuntimeCache.CurrentHomeServer == null) { NavigationManager.NavigateTo("/Login"); @@ -208,21 +222,49 @@ else private async Task LoadStatesAsync() { - // using var client = new HttpClient(); - // client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", LocalStorageWrapper.AccessToken); - // var response = await client.GetAsync($"{LocalStorageWrapper.CurrentHomeserver}/_matrix/client/r0/rooms/{RoomId}/state"); - // var content = await response.Content.ReadAsStringAsync(); - // Console.WriteLine(JsonSerializer.Deserialize<object>(content).ToJson()); - // var stateEvents = JsonSerializer.Deserialize<List<StateEvent>>(content); + // using var client = new HttpClient(); + // client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", LocalStorageWrapper.AccessToken); + // var response = await client.GetAsync($"{LocalStorageWrapper.CurrentHomeserver}/_matrix/client/r0/rooms/{RoomId}/state"); + // var Content = await response.Content.ReadAsStringAsync(); + // Console.WriteLine(JsonSerializer.Deserialize<object>(Content).ToJson()); + // var stateEvents = JsonSerializer.Deserialize<List<StateEventResponse>>(Content); var room = await RuntimeCache.CurrentHomeServer.GetRoom(RoomId); var stateEventsQuery = await room.GetStateAsync(""); if (stateEventsQuery == null) { Console.WriteLine("state events query is null!!!"); } - var stateEvents = stateEventsQuery.Value.Deserialize<List<StateEvent>>(); - PolicyEvents = stateEvents.Where(x => x.type.StartsWith("m.policy.rule")) - .Select(x => JsonSerializer.Deserialize<StateEvent<PolicyRuleStateEventData>>(JsonSerializer.Serialize(x))).ToList(); + var stateEvents = stateEventsQuery.Value.Deserialize<List<StateEventResponse>>(); + PolicyEvents = stateEvents.Where(x => x.Type.StartsWith("m.policy.rule")) + .Select(x => JsonSerializer.Deserialize<StateEventResponse<PolicyRuleStateEventData>>(JsonSerializer.Serialize(x))).ToList(); + StateHasChanged(); + } + + private async Task GetAvatar(string userId) + { + try + { + if (avatars.ContainsKey(userId)) return; + var hs = userId.Split(':')[1]; + RemoteHomeServer server = servers.ContainsKey(hs) ? servers[hs] : await new RemoteHomeServer(userId.Split(':')[1]).Configure(); + if (!servers.ContainsKey(hs)) servers.Add(hs, server); + var profile = await server.GetProfile(userId); + avatars.Add(userId, server.ResolveMediaUri(profile.AvatarUrl)); + servers.Add(userId, server); + StateHasChanged(); + } + catch + { + // ignored + } + } + + private async Task GetAllAvatars() + { + foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && x.content.Entity != null)) + { + await GetAvatar(policyEvent.content.Entity); + } StateHasChanged(); } diff --git a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor @@ -35,7 +35,7 @@ else @code { //get room list // - sync withroom list filter - // type = support.feline.msc3784 + // Type = support.feline.msc3784 //support.feline.policy.lists.msc.v1 public List<PolicyRoomInfo> PolicyRoomList { get; set; } = new(); @@ -45,7 +45,7 @@ else protected override async Task OnInitializedAsync() { - if (!RuntimeCache.WasLoaded) await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); + await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); await base.OnInitializedAsync(); if (RuntimeCache.CurrentHomeServer == null) { diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor @@ -11,21 +11,28 @@ else { <p>You are in @Rooms.Count rooms and @Spaces.Count spaces</p> + <p> + <a href="/RoomManagerCreateRoom">Create room</a> + </p> + <details open> <summary>Space List</summary> @foreach (var room in Spaces) { - <a style="color: unset; text-decoration: unset;" href="/RoomManager/Space/@room.RoomId.Replace('.', '~')"><RoomListItem Room="@room" ShowOwnProfile="true"></RoomListItem></a> + <a style="color: unset; text-decoration: unset;" href="/RoomManager/Space/@room.RoomId.Replace('.', '~')"> + <RoomListItem Room="@room" ShowOwnProfile="true"></RoomListItem> + </a> } </details> <details open> <summary>Room List</summary> @foreach (var room in Rooms) { - <a style="color: unset; text-decoration: unset;" href="/RoomManager/Room/@room.RoomId.Replace('.', '~')"><RoomListItem Room="@room" ShowOwnProfile="true"></RoomListItem></a> + <a style="color: unset; text-decoration: unset;" href="/RoomManager/Room/@room.RoomId.Replace('.', '~')"> + <RoomListItem Room="@room" ShowOwnProfile="true"></RoomListItem> + </a> } </details> - } <div style="margin-bottom: 4em;"></div> @@ -34,9 +41,10 @@ else @code { public List<Room> Rooms { get; set; } = new(); public List<Room> Spaces { get; set; } = new(); + protected override async Task OnInitializedAsync() { - if (!RuntimeCache.WasLoaded) await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); + await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); await base.OnInitializedAsync(); if (RuntimeCache.CurrentHomeServer == null) { @@ -45,40 +53,47 @@ else } Rooms = await RuntimeCache.CurrentHomeServer.GetJoinedRooms(); StateHasChanged(); - var semaphore = new SemaphoreSlim(1000); + Console.WriteLine($"Got {Rooms.Count} rooms"); + var semaphore = new SemaphoreSlim(10); var tasks = new List<Task<Room?>>(); foreach (var room in Rooms) { tasks.Add(CheckIfSpace(room, semaphore)); } await Task.WhenAll(tasks); - + Console.WriteLine("Fetched joined rooms!"); } - + private async Task<Room?> CheckIfSpace(Room room, SemaphoreSlim semaphore) { await semaphore.WaitAsync(); + // Console.WriteLine($"Checking if {room.RoomId} is a space"); try { - var state = await room.GetStateAsync("m.room.create"); + var state = await room.GetStateAsync<CreateEvent>("m.room.create"); if (state != null) { - //Console.WriteLine(state.Value.ToJson()); - if(state.Value.TryGetProperty("type", out var type)) + //Console.WriteLine(state.Value.ToJson()); + if (state.Type != null) { - if(type.ToString() == "m.space") + if (state.Type == "m.space") { + Console.WriteLine($"Room {room.RoomId} is a space!"); Spaces.Add(room); Rooms.Remove(room); StateHasChanged(); return room; } + else + { + Console.WriteLine($"Encountered unknown room type {state.Type}"); + } } else { - //this is fine, apprently... - //Console.WriteLine($"Room {room.RoomId} has no content.type in m.room.create!"); + //this is fine, apprently... + // Console.WriteLine($"Room {room.RoomId} has no Content.type in m.room.create!"); } } } @@ -93,4 +108,5 @@ else } return null; } -} -\ No newline at end of file + +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor @@ -0,0 +1,298 @@ +@page "/RoomManagerCreateRoom" +@using System.Text.Json +@using MatrixRoomUtils.Core.Extensions +@using MatrixRoomUtils.Core.Responses +@using System.Runtime.Intrinsics.X86 +<h3>Room Manager - Create Room</h3> + +@* <pre Contenteditable="true" @onkeypress="@JsonChanged" ="JsonString">@JsonString</pre> *@ +<table> + <tr > + <td style="padding-bottom: 16px;">Preset:</td> + <td style="padding-bottom: 16px;"> + <InputSelect @bind-Value="@RoomPreset"> + @foreach (var createRoomRequest in Presets) + { + <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> + + <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 style="vertical-align: top;">Initial states:</td> + <td> + <details> + @code{ + + private static string[] ImplementedStates = new[] { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", }; + + } + <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> + <td style="vertical-align: top;">@(initialState.Type):</td> + + <td> + <pre>@JsonSerializer.Serialize(initialState.Content, new JsonSerializerOptions { WriteIndented = true })</pre> + </td> + </tr> + } + </table> + </details> + </td> + </tr> +</table> +<br/> +<details> + <summary>Creation JSON</summary> + <pre> + @creationEvent.ToJson(ignoreNull: true) + </pre> +</details> +<details open> + <summary>Creation JSON (with null values)</summary> + <EditablePre @bind-Value="@JsonString" oninput="@JsonChanged"></EditablePre> +</details> + + +@code { + + private string JsonString + { + get => creationEvent.ToJson(); + set + { + creationEvent = JsonSerializer.Deserialize<CreateRoomRequest>(value); + JsonChanged(); + } + } + + private string RoomPreset + { + get + { + if (Presets.ContainsValue(creationEvent)) + { + return Presets.First(x => x.Value == creationEvent).Key; + } + return "Not a preset"; + } + set + { + creationEvent = Presets[value]; + JsonChanged(); + } + } + + private Dictionary<string, string> creationEventValidationErrors { get; set; } = new(); + + private CreateRoomRequest creationEvent { get; set; } + + private Dictionary<string, CreateRoomRequest> Presets { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); + + creationEvent = Presets["Default room"] = new CreateRoomRequest + { + 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 } + } + }; + + + await base.OnInitializedAsync(); + } + + private void JsonChanged() + { + Console.WriteLine(JsonString); + } + + + private string GetStateFriendlyName(string key) => key switch { + "m.room.history_visibility" => "History visibility", + "m.room.guest_access" => "Guest access", + "m.room.join_rules" => "Join rules", + "m.room.server_acl" => "Server ACL", + "m.room.avatar" => "Avatar", + _ => key + }; + +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor @@ -13,9 +13,9 @@ <br/> <details style="background: #0002;"> <summary style="background: #fff1;">State list</summary> - @foreach (var stateEvent in States.OrderBy(x => x.state_key).ThenBy(x => x.type)) + @foreach (var stateEvent in States.OrderBy(x => x.StateKey).ThenBy(x => x.Type)) { - <p>@stateEvent.state_key/@stateEvent.type:</p> + <p>@stateEvent.StateKey/@stateEvent.Type:</p> <pre>@stateEvent.content.ToJson()</pre> } </details> @@ -27,7 +27,7 @@ private Room? Room { get; set; } - private StateEvent<object>[] States { get; set; } = Array.Empty<StateEvent<object>>(); + private StateEventResponse<object>[] States { get; set; } = Array.Empty<StateEventResponse<object>>(); private List<Room> Rooms { get; set; } = new(); protected override async Task OnInitializedAsync() @@ -38,14 +38,14 @@ if (state != null) { Console.WriteLine(state.Value.ToJson()); - States = state.Value.Deserialize<StateEvent<object>[]>()!; + States = state.Value.Deserialize<StateEventResponse<object>[]>()!; foreach (var stateEvent in States) { - if (stateEvent.type == "m.space.child") + if (stateEvent.Type == "m.space.child") { - // if (stateEvent.content.ToJson().Length < 5) return; - var roomId = stateEvent.state_key; + // if (stateEvent.Content.ToJson().Length < 5) return; + var roomId = stateEvent.StateKey; var room = await RuntimeCache.CurrentHomeServer.GetRoom(roomId); if (room != null) { @@ -54,13 +54,13 @@ } } - // if(state.Value.TryGetProperty("type", out var type)) + // if(state.Value.TryGetProperty("Type", out var Type)) // { // } // else // { // //this is fine, apprently... - // //Console.WriteLine($"Room {room.RoomId} has no content.type in m.room.create!"); + // //Console.WriteLine($"Room {room.RoomId} has no Content.Type in m.room.create!"); // } } await base.OnInitializedAsync(); diff --git a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor @@ -12,7 +12,7 @@ <br/> <InputSelect @bind-Value="shownStateKey"> <option value="">-- State key --</option> - @foreach (var stateEvent in FilteredEvents.Where(x => x.state_key != "").Select(x => x.state_key).Distinct().OrderBy(x => x)) + @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey != "").Select(x => x.StateKey).Distinct().OrderBy(x => x)) { <option value="@stateEvent">@stateEvent</option> Console.WriteLine(stateEvent); @@ -21,33 +21,33 @@ <br/> <InputSelect @bind-Value="shownType"> <option value="">-- Type --</option> - @foreach (var stateEvent in FilteredEvents.Where(x => x.state_key != shownStateKey).Select(x => x.type).Distinct().OrderBy(x => x)) + @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey != shownStateKey).Select(x => x.Type).Distinct().OrderBy(x => x)) { <option value="@stateEvent">@stateEvent</option> } </InputSelect> <br/> -<textarea @bind="shownEventJson" style="width: 100%; height: fit-content;"></textarea> +<textarea @bind="shownEventJson" style="width: 100%; height: fit-Content;"></textarea> <LogView></LogView> @code { //get room list // - sync withroom list filter - // type = support.feline.msc3784 + // Type = support.feline.msc3784 //support.feline.policy.lists.msc.v1 [Parameter] public string? RoomId { get; set; } - public List<StateEvent> FilteredEvents { get; set; } = new(); - public List<StateEvent> Events { get; set; } = new(); + public List<StateEventResponse> FilteredEvents { get; set; } = new(); + public List<StateEventResponse> Events { get; set; } = new(); public string status = ""; protected override async Task OnInitializedAsync() { - if (!RuntimeCache.WasLoaded) await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); + await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); await base.OnInitializedAsync(); if (RuntimeCache.CurrentHomeServer != null) { @@ -71,18 +71,18 @@ // var response = await client.GetAsync($"http://localhost:5117/matrix-hq-state.json"); //var _events = await response.Content.ReadFromJsonAsync<Queue<StateEventStruct>>(); var _data = await response.Content.ReadAsStreamAsync(); - var __events = JsonSerializer.DeserializeAsyncEnumerable<StateEvent>(_data); + var __events = JsonSerializer.DeserializeAsyncEnumerable<StateEventResponse>(_data); await foreach (var _ev in __events) { - var e = new StateEvent() + var e = new StateEventResponse() { - type = _ev.type, - state_key = _ev.state_key, - origin_server_ts = _ev.origin_server_ts, - content = _ev.content + Type = _ev.Type, + StateKey = _ev.StateKey, + OriginServerTs = _ev.OriginServerTs, + Content = _ev.Content }; Events.Add(e); - if (string.IsNullOrEmpty(e.state_key)) + if (string.IsNullOrEmpty(e.StateKey)) { FilteredEvents.Add(e); } @@ -106,7 +106,7 @@ await Task.Delay(1); var _FilteredEvents = Events; if (!ShowMembershipEvents) - _FilteredEvents = _FilteredEvents.Where(x => x.type != "m.room.member").ToList(); + _FilteredEvents = _FilteredEvents.Where(x => x.Type != "m.room.member").ToList(); status = "Done, rerendering!"; StateHasChanged(); @@ -114,7 +114,7 @@ FilteredEvents = _FilteredEvents; if(_shownType != null) - shownEventJson = _FilteredEvents.Where(x => x.type == _shownType).First().content.ToJson(indent: true, ignoreNull: true); + shownEventJson = _FilteredEvents.Where(x => x.Type == _shownType).First().Content.ToJson(indent: true, ignoreNull: true); StateHasChanged(); } @@ -126,10 +126,10 @@ public long origin_server_ts { get; set; } public string state_key { get; set; } public string type { get; set; } - // public string sender { get; set; } - // public string event_id { get; set; } - // public string user_id { get; set; } - // public string replaces_state { get; set; } + // public string Sender { get; set; } + // public string EventId { get; set; } + // public string UserId { get; set; } + // public string ReplacesState { get; set; } } public bool ShowMembershipEvents diff --git a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateRoomList.razor b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateRoomList.razor @@ -23,7 +23,7 @@ else public List<string> Rooms { get; set; } = new(); protected override async Task OnInitializedAsync() { - if (!RuntimeCache.WasLoaded) await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); + await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); await base.OnInitializedAsync(); if (RuntimeCache.CurrentHomeServer == null) { diff --git a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor @@ -11,7 +11,7 @@ <input type="checkbox" id="showAll" @bind="ShowMembershipEvents"/> Show member events -<table class="table table-striped table-hover" style="width: fit-content;"> +<table class="table table-striped table-hover" style="width: fit-Content;"> <thead> <tr> <th scope="col">Type</th> @@ -23,7 +23,7 @@ { <tr> <td>@stateEvent.type</td> - <td style="max-width: fit-content;"> + <td style="max-width: fit-Content;"> <pre>@stateEvent.content</pre> </td> </tr> @@ -35,7 +35,7 @@ { <details> <summary>@group.Key</summary> - <table class="table table-striped table-hover" style="width: fit-content;"> + <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> <tr> <th scope="col">Type</th> @@ -47,7 +47,7 @@ { <tr> <td>@stateEvent.type</td> - <td style="max-width: fit-content;"> + <td style="max-width: fit-Content;"> <pre>@stateEvent.content</pre> </td> </tr> @@ -62,7 +62,7 @@ @code { //get room list // - sync withroom list filter - // type = support.feline.msc3784 + // Type = support.feline.msc3784 //support.feline.policy.lists.msc.v1 [Parameter] @@ -74,7 +74,7 @@ protected override async Task OnInitializedAsync() { - if (!RuntimeCache.WasLoaded) await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); + await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); await base.OnInitializedAsync(); if (RuntimeCache.CurrentHomeServer == null) { @@ -149,10 +149,10 @@ public long origin_server_ts { get; set; } public string state_key { get; set; } public string type { get; set; } - // public string sender { get; set; } - // public string event_id { get; set; } - // public string user_id { get; set; } - // public string replaces_state { get; set; } + // public string Sender { get; set; } + // public string EventId { get; set; } + // public string UserId { get; set; } + // public string ReplacesState { get; set; } } public bool ShowMembershipEvents diff --git a/MatrixRoomUtils.Web/Pages/UserImportPage.razor b/MatrixRoomUtils.Web/Pages/UserImportPage.razor @@ -1,71 +0,0 @@ -@page "/ImportUsers" -@using System.Text.Json -@using MatrixRoomUtils.Core.Authentication -@inject ILocalStorageService LocalStorage -<h3>Login</h3> - -<InputFile OnChange="@FileChanged" accept=".tsv"></InputFile> -<br/> -<button @onclick="Login">Login</button> -<br/><br/> -<h4>Parsed records</h4> -<hr/> -<table border="1"> - @foreach (var (homeserver, username, password) in records) - { - <tr style="background-color: @(RuntimeCache.LoginSessions.Any(x => x.Value.LoginResponse.UserId == $"@{username}:{homeserver}") ? "green" : "unset")"> - <td style="border-width: 1px;">@username</td> - <td style="border-width: 1px;">@homeserver</td> - <td style="border-width: 1px;">@password.Length chars</td> - </tr> - } -</table> -<br/> -<br/> -<LogView></LogView> - -@code { - List<(string homeserver, string username, string password)> records = new(); - - async Task Login() - { - foreach (var (homeserver, username, password) in records) - { - if(RuntimeCache.LoginSessions.Any(x => x.Value.LoginResponse.UserId == $"@{username}:{homeserver}")) continue; - var result = await MatrixAuth.Login(homeserver, username, password); - Console.WriteLine($"Obtained access token for {result.UserId}!"); - - var userinfo = new UserInfo() - { - LoginResponse = result - }; - userinfo.Profile = await MatrixAuth.GetProfile(result.HomeServer, result.UserId); - RuntimeCache.LastUsedToken = result.AccessToken; - - RuntimeCache.LoginSessions.Add(result.AccessToken, userinfo); - StateHasChanged(); - } - - await LocalStorageWrapper.SaveToLocalStorage(LocalStorage); - } - - private async Task FileChanged(InputFileChangeEventArgs obj) - { - Console.WriteLine(JsonSerializer.Serialize(obj, new JsonSerializerOptions() - { - WriteIndented = true - })); - await using var rs = obj.File.OpenReadStream(); - using var sr = new StreamReader(rs); - string TsvData = await sr.ReadToEndAsync(); - records.Clear(); - foreach (var line in TsvData.Split('\n')) - { - var parts = line.Split('\t'); - if (parts.Length != 3) - continue; - records.Add((parts[0], parts[1], parts[2])); - } - } - -} -\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Shared/EditablePre.razor b/MatrixRoomUtils.Web/Shared/EditablePre.razor @@ -0,0 +1,18 @@ +@inherits InputBase<string> +<pre id="@Id" class="@CssClass" @onkeyup="Callback" contenteditable="true">@CurrentValue</pre> +@code { + protected override bool TryParseValueFromString(string? value, out string result, out string? validationErrorMessage) + { + result = value; + validationErrorMessage = null; + return true; + } + + public object Id { get; set; } + + private async Task Callback() + { + Console.WriteLine("beep"); + } + +} +\ No newline at end of file diff --git a/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor b/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor @@ -31,7 +31,7 @@ { var hs = await new AuthenticatedHomeServer(User.LoginResponse.UserId, User.AccessToken, User.LoginResponse.HomeServer).Configure(); if (User.Profile.AvatarUrl != null && User.Profile.AvatarUrl != "") - _avatarUrl = await hs.ResolveMediaUri(User.Profile.AvatarUrl); + _avatarUrl = hs.ResolveMediaUri(User.Profile.AvatarUrl); else _avatarUrl = "https://api.dicebear.com/6.x/identicon/svg?seed=" + User.LoginResponse.UserId; _roomCount = (await hs.GetJoinedRooms()).Count; await base.OnInitializedAsync(); diff --git a/MatrixRoomUtils.Web/Shared/MainLayout.razor b/MatrixRoomUtils.Web/Shared/MainLayout.razor @@ -17,7 +17,7 @@ } </div> - <article class="content px-4"> + <article class="Content px-4"> @Body </article> </main> diff --git a/MatrixRoomUtils.Web/Shared/RoomListItem.razor b/MatrixRoomUtils.Web/Shared/RoomListItem.razor @@ -1,7 +1,7 @@ @using MatrixRoomUtils.Core.Authentication @using System.Text.Json @using MatrixRoomUtils.Core.Extensions -<div style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-content; @(hasDangerousRoomVersion ? "border: red 4px solid;" : hasOldRoomVersion ? "border: #FF0 1px solid;" : "")"> +<div style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-Content; @(hasDangerousRoomVersion ? "border: red 4px solid;" : hasOldRoomVersion ? "border: #FF0 1px solid;" : "")"> @if (ShowOwnProfile) { <img style="@(ChildContent != null ? "vertical-align: baseline;":"") width: 32px; height: 32px; border-radius: 50%; @(hasCustomProfileAvatar ? "border-color: red; border-width: 3px; border-style: dashed;" : "")" src="@profileAvatar"/> @@ -96,7 +96,7 @@ var url = state.Value.GetProperty("url").GetString(); if (url != null) { - roomIcon = await RuntimeCache.CurrentHomeServer.ResolveMediaUri(url); + roomIcon = RuntimeCache.CurrentHomeServer.ResolveMediaUri(url); } } catch (InvalidOperationException e) @@ -116,7 +116,7 @@ if (_avatar.ValueKind == JsonValueKind.String) { hasCustomProfileAvatar = _avatar.GetString() != profile.AvatarUrl; - profileAvatar = await RuntimeCache.CurrentHomeServer.ResolveMediaUri(_avatar.GetString()); + profileAvatar = RuntimeCache.CurrentHomeServer.ResolveMediaUri(_avatar.GetString()); } else { diff --git a/MatrixRoomUtils.Web/wwwroot/css/app.css b/MatrixRoomUtils.Web/wwwroot/css/app.css @@ -1,5 +1,9 @@ @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); +article > h3:first-child { + padding-top: 24px; +} + html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #222; diff --git a/MatrixRoomUtils.Web/wwwroot/index.html b/MatrixRoomUtils.Web/wwwroot/index.html @@ -2,31 +2,42 @@ <html lang="en"> <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/> <title>MatrixRoomUtils.Web</title> - <base href="/" /> - <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> - <link href="css/app.css" rel="stylesheet" /> - <link rel="icon" type="image/png" href="favicon.png" /> - <link href="MatrixRoomUtils.Web.styles.css" rel="stylesheet" /> + <base href="/"/> + <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet"/> + <link href="css/app.css" rel="stylesheet"/> + <link rel="icon" type="image/png" href="favicon.png"/> + <link href="MatrixRoomUtils.Web.styles.css" rel="stylesheet"/> </head> <body> - <div id="app"> - <svg class="loading-progress"> - <circle r="40%" cx="50%" cy="50%" /> - <circle r="40%" cx="50%" cy="50%" /> - </svg> - <div class="loading-progress-text"></div> - </div> +<div id="app"> + <svg class="loading-progress"> + <circle r="40%" cx="50%" cy="50%"/> + <circle r="40%" cx="50%" cy="50%"/> + </svg> + <div class="loading-progress-text"></div> +</div> - <div id="blazor-error-ui"> - An unhandled error has occurred. - <a href="" class="reload">Reload</a> - <a class="dismiss">🗙</a> - </div> - <script src="_framework/blazor.webassembly.js"></script> +<div id="blazor-error-ui"> + An unhandled error has occurred. + <a href="" class="reload">Reload</a> + <a class="dismiss">🗙</a> +</div> +<script> + function BlazorFocusElement(element) { + if (element == null) return; + if (element instanceof HTMLElement) { + console.log(element); + element.focus(); + } else { + console.log("Element is not an HTMLElement", element); + } + } +</script> +<script src="_framework/blazor.webassembly.js"></script> </body> </html>