about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--MatrixRoomUtils.Core/AuthenticatedHomeServer.cs12
-rw-r--r--MatrixRoomUtils.Core/Authentication/MatrixAuth.cs3
-rw-r--r--MatrixRoomUtils.Core/Interfaces/IHomeServer.cs2
-rw-r--r--MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs217
-rw-r--r--MatrixRoomUtils.Core/Responses/StateEventResponse.cs34
-rw-r--r--MatrixRoomUtils.Core/Room.cs34
-rw-r--r--MatrixRoomUtils.Core/RuntimeCache.cs36
-rw-r--r--MatrixRoomUtils.Core/StateEvent.cs71
-rw-r--r--MatrixRoomUtils.Web.Server/Pages/Error.cshtml2
-rw-r--r--MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs9
-rw-r--r--MatrixRoomUtils.Web/FileUploadTest.razor19
-rw-r--r--MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj10
-rw-r--r--MatrixRoomUtils.Web/Pages/DataExportPage.razor43
-rw-r--r--MatrixRoomUtils.Web/Pages/DebugTools.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/DevOptions.razor3
-rw-r--r--MatrixRoomUtils.Web/Pages/Index.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor14
-rw-r--r--MatrixRoomUtils.Web/Pages/LoginPage.razor119
-rw-r--r--MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor122
-rw-r--r--MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor4
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor44
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor298
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor18
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor40
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomState/RoomStateRoomList.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor20
-rw-r--r--MatrixRoomUtils.Web/Pages/UserImportPage.razor71
-rw-r--r--MatrixRoomUtils.Web/Shared/EditablePre.razor18
-rw-r--r--MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor2
-rw-r--r--MatrixRoomUtils.Web/Shared/MainLayout.razor2
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomListItem.razor6
-rw-r--r--MatrixRoomUtils.Web/wwwroot/css/app.css4
-rw-r--r--MatrixRoomUtils.Web/wwwroot/index.html51
33 files changed, 1004 insertions, 330 deletions
diff --git a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
index 7a1f5de..502fe5b 100644
--- 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
index 7bcd75f..b4b8d19 100644
--- 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
index 8fb8b2c..9f7bfee 100644
--- 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
new file mode 100644
index 0000000..6949b1a
--- /dev/null
+++ 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
new file mode 100644
index 0000000..d86f546
--- /dev/null
+++ 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
index 362abf4..b96546e 100644
--- 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
index 4e756a7..380a150 100644
--- 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
index df7267d..2201587 100644
--- 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
index 5e41c43..0125c85 100644
--- 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
index 4a00a8a..bd44f7f 100644
--- 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
new file mode 100644
index 0000000..2e25b54
--- /dev/null
+++ 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
index 77a039c..12555c3 100644
--- 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
index 6e1b5ec..58e4111 100644
--- 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
index ffa2134..c8fabaa 100644
--- 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
index e1b6ac0..9ade1b8 100644
--- 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
index 7be4149..8be8570 100644
--- 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
index 882dd1e..13b717d 100644
--- 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
index c986d40..9fcedd1 100644
--- 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
index d0f9b87..08cdc2c 100644
--- 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
index f25fbae..e61598a 100644
--- 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
index 6d27679..35bf501 100644
--- 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
new file mode 100644
index 0000000..7b4db37
--- /dev/null
+++ 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
index 4a5bddf..a44b2b4 100644
--- 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
index 638d728..3037dcc 100644
--- 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
index ba1b0ec..c654b13 100644
--- 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
index 0f40cb9..c7f9f3c 100644
--- 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
deleted file mode 100644
index 6f3045e..0000000
--- a/MatrixRoomUtils.Web/Pages/UserImportPage.razor
+++ /dev/null
@@ -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
new file mode 100644
index 0000000..01bea0d
--- /dev/null
+++ 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
index 87ef831..016b993 100644
--- 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
index b1b0b53..cdb1205 100644
--- 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
index 317d25a..b7394c1 100644
--- 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
index acbbfce..c6d71d1 100644
--- 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
index 580a88b..028e56b 100644
--- 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>