diff --git a/MatrixRoomUtils.Web/Pages/DataExportPage.razor b/MatrixRoomUtils.Web/Pages/DataExportPage.razor
new file mode 100644
index 0000000..a7c6f6b
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/DataExportPage.razor
@@ -0,0 +1,83 @@
+@page "/export"
+@using MatrixRoomUtils.Web.Classes
+@using MatrixRoomUtils.Web.Shared.IndexComponents
+@using Blazored.LocalStorage
+@using MatrixRoomUtils.Authentication
+@using System.Text.Json
+@using Microsoft.AspNetCore.Components.Rendering
+@inject NavigationManager NavigationManager
+@inject ILocalStorageService LocalStorage
+
+<PageTitle>Export</PageTitle>
+
+<h1>Data export</h1>
+
+<br/><br/>
+<h5>Signed in accounts - <a href="/Login">Add new account</a> or <a href="/ImportUsers">Import from TSV</a></h5>
+<hr/>
+@if (_isLoaded)
+{
+@foreach (var (token, user) in RuntimeStorage.UsersCache)
+{
+ <IndexUserItem User="@user" Token="@token"/>
+ <pre>
+@user.LoginResponse.UserId[1..].Split(":")[0]\auth\access_token=@token
+@user.LoginResponse.UserId[1..].Split(":")[0]\auth\device_id=@user.LoginResponse.DeviceId
+@user.LoginResponse.UserId[1..].Split(":")[0]\auth\home_server=@(RuntimeStorage.HomeserverResolutionCache.ContainsKey(user.LoginResponse.HomeServer) ? RuntimeStorage.HomeserverResolutionCache[user.LoginResponse.HomeServer].Result : "loading...")
+@user.LoginResponse.UserId[1..].Split(":")[0]\auth\user_id=@@@user.LoginResponse.UserId
+@user.LoginResponse.UserId[1..].Split(":")[0]\user\automatically_share_keys_with_trusted_users=true
+@user.LoginResponse.UserId[1..].Split(":")[0]\user\muted_tags=global
+@user.LoginResponse.UserId[1..].Split(":")[0]\user\online_key_backup=true
+@user.LoginResponse.UserId[1..].Split(":")[0]\user\only_share_keys_with_verified_users=false
+ </pre>
+}
+}
+else
+{
+ <p>Loading...</p>
+ <p>@resolvedHomeservers/@totalHomeservers homeservers resolved...</p>
+}
+
+@code {
+ private bool _isLoaded;
+ private int resolvedHomeservers;
+ private int totalHomeservers;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await base.OnInitializedAsync();
+ Console.WriteLine("Users in cache: " + RuntimeStorage.UsersCache.Count);
+ if (!RuntimeStorage.WasLoaded)
+ {
+ Console.WriteLine("[INDEX] !!! LOCALSTORAGE WAS NOT LOADED !!!");
+ await RuntimeStorage.LoadFromLocalStorage(LocalStorage);
+
+ Console.WriteLine("Users in cache: " + RuntimeStorage.UsersCache.Count);
+
+ var homeservers = RuntimeStorage.UsersCache.Values.Select(x => x.LoginResponse.HomeServer).Distinct();
+ totalHomeservers = homeservers.Count();
+ StateHasChanged();
+ foreach (var hs in homeservers)
+ {
+ if (RuntimeStorage.HomeserverResolutionCache.ContainsKey(hs)) continue;
+ var resolvedHomeserver = await MatrixAccount.ResolveHomeserverFromWellKnown(hs);
+
+ if (RuntimeStorage.HomeserverResolutionCache.ContainsKey(hs))
+ RuntimeStorage.HomeserverResolutionCache.Remove(hs);
+ RuntimeStorage.HomeserverResolutionCache.Add(hs, new(){Result = resolvedHomeserver, ResolutionTime = DateTime.Now});
+ await RuntimeStorage.SaveToLocalStorage(LocalStorage);
+
+ Console.WriteLine("Saved to local storage:");
+ Console.WriteLine(JsonSerializer.Serialize(RuntimeStorage.HomeserverResolutionCache, new JsonSerializerOptions()
+ {
+ WriteIndented = true
+ }));
+ resolvedHomeservers++;
+ StateHasChanged();
+ }
+ StateHasChanged();
+ }
+ _isLoaded = true;
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/Index.razor b/MatrixRoomUtils.Web/Pages/Index.razor
new file mode 100644
index 0000000..7e9facf
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/Index.razor
@@ -0,0 +1,33 @@
+@page "/"
+@using MatrixRoomUtils.Web.Classes
+@using MatrixRoomUtils.Web.Shared.IndexComponents
+@using Blazored.LocalStorage
+@inject NavigationManager NavigationManager
+@inject ILocalStorageService LocalStorage
+
+<PageTitle>Index</PageTitle>
+
+<h1>Rory&::MatrixUtils</h1>
+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>
+<hr/>
+@{
+ Console.WriteLine("Users in cache: " + RuntimeStorage.UsersCache.Count);
+ if (!RuntimeStorage.WasLoaded)
+ {
+ Console.WriteLine("[INDEX] !!! LOCALSTORAGE WAS NOT LOADED !!!");
+ RuntimeStorage.LoadFromLocalStorage(LocalStorage).GetAwaiter().OnCompleted(() =>
+ {
+ Console.WriteLine("Users in cache: " + RuntimeStorage.UsersCache.Count);
+ StateHasChanged();
+ });
+ }
+}
+<form>
+ @foreach (var (token, user) in RuntimeStorage.UsersCache)
+ {
+ <IndexUserItem User="@user" Token="@token"/>
+ }
+</form>
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/LoginPage.razor b/MatrixRoomUtils.Web/Pages/LoginPage.razor
new file mode 100644
index 0000000..d193f95
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/LoginPage.razor
@@ -0,0 +1,46 @@
+@page "/Login"
+@using MatrixRoomUtils.Authentication
+@using MatrixRoomUtils.Web.Classes
+@using Blazored.LocalStorage
+@inject ILocalStorageService LocalStorage
+<h3>Login</h3>
+
+<label>Homeserver:</label>
+<input @bind="homeserver"/>
+<br/>
+<label>Username:</label>
+<input @bind="username"/>
+<br/>
+<label>Password:</label>
+<input @bind="password" type="password"/>
+<br/>
+<button @onclick="Login">Login</button>
+<br/>
+<br/>
+<LogView></LogView>
+
+@code {
+ string homeserver = "";
+ string username = "";
+ string password = "";
+ async Task Login()
+ {
+ var result = await MatrixAccount.Login(homeserver, username, password);
+ Console.WriteLine($"Obtained access token for {result.UserId}!");
+
+ RuntimeStorage.AccessToken = result.AccessToken;
+
+ var userinfo = new UserInfo()
+ {
+ LoginResponse = result
+ };
+ userinfo.Profile = await MatrixAccount.GetProfile(result.HomeServer, result.UserId);
+
+ RuntimeStorage.UsersCache.Add(result.AccessToken, userinfo);
+ RuntimeStorage.CurrentHomeserver = await MatrixAccount.ResolveHomeserverFromWellKnown(result.HomeServer);
+
+
+ await RuntimeStorage.SaveToLocalStorage(LocalStorage);
+
+ }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/PolicyListEditorPage.razor b/MatrixRoomUtils.Web/Pages/PolicyListEditorPage.razor
new file mode 100644
index 0000000..cedaf32
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/PolicyListEditorPage.razor
@@ -0,0 +1,232 @@
+@page "/PolicyListEditor/{RoomId}"
+@using MatrixRoomUtils.Authentication
+@using MatrixRoomUtils.Web.Classes
+@using Blazored.LocalStorage
+@using System.Net.Http.Headers
+@using System.Text.Json
+@using MatrixRoomUtils.Extensions
+@using MatrixRoomUtils.StateEventTypes
+@inject ILocalStorageService LocalStorage
+@inject NavigationManager NavigationManager
+<h3>Policy list editor</h3>
+
+<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.
+</p>
+
+
+@if (!PolicyEvents.Any(x => x.type == "m.policy.rule.server"))
+{
+ <p>No server policies</p>
+}
+else
+{
+ <h3>Server policies</h3>
+ <hr/>
+ <table class="table table-striped table-hover" style="width: fit-content;">
+ <thead>
+ <tr>
+ <th scope="col" style="max-width: 50vw;">Server</th>
+ <th scope="col">Reason</th>
+ <th scope="col">Expires</th>
+ <th scope="col">Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ @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>@policyEvent.content.Reason</td>
+ <td>
+ @policyEvent.content.ExpiryDateTime
+ </td>
+ <td>
+ <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ <details>
+ <summary>Invalid events</summary>
+ <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>
+ </tr>
+ </thead>
+ <tbody>
+ @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.content.ToJson(indent: false, ignoreNull: true)</td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+}
+@if (!PolicyEvents.Any(x => x.type == "m.policy.rule.room"))
+{
+ <p>No room policies</p>
+}
+else
+{
+ <h3>Room policies</h3>
+ <hr/>
+ <table class="table table-striped table-hover" style="width: fit-content;">
+ <thead>
+ <tr>
+ <th scope="col" style="max-width: 50vw;">Room</th>
+ <th scope="col">Reason</th>
+ <th scope="col">Expires</th>
+ <th scope="col">Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ @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>@policyEvent.content.Reason</td>
+ <td>
+ @policyEvent.content.ExpiryDateTime
+ </td>
+ <td>
+ <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ <details>
+ <summary>Invalid events</summary>
+ <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>
+ </tr>
+ </thead>
+ <tbody>
+ @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.content.ToJson(indent: false, ignoreNull: true)</td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+}
+@if (!PolicyEvents.Any(x => x.type == "m.policy.rule.user"))
+{
+ <p>No user policies</p>
+}
+else
+{
+ <h3>User policies</h3>
+ <hr/>
+ <table class="table table-striped table-hover" style="width: fit-content;">
+ <thead>
+ <tr>
+ <th scope="col" style="max-width: 0.2vw; word-wrap: anywhere;">User</th>
+ <th scope="col">Reason</th>
+ <th scope="col">Expires</th>
+ <th scope="col">Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ @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>
+ <td>@policyEvent.content.Reason</td>
+ <td>
+ @policyEvent.content.ExpiryDateTime
+ </td>
+ <td>
+ <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ <details>
+ <summary>Invalid events</summary>
+ <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>
+ </tr>
+ </thead>
+ <tbody>
+ @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.content.ToJson(indent: false, ignoreNull: true)</td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+}
+
+<LogView></LogView>
+
+@code {
+ //get room list
+ // - sync withroom list filter
+ // type = support.feline.msc3784
+ //support.feline.policy.lists.msc.v1
+
+ [Parameter]
+ public string? RoomId { get; set; }
+
+ public List<StateEvent<PolicyRuleStateEventData>> PolicyEvents { get; set; } = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (!RuntimeStorage.WasLoaded) await RuntimeStorage.LoadFromLocalStorage(LocalStorage);
+ await base.OnInitializedAsync();
+ if(RuntimeStorage.AccessToken == null || RuntimeStorage.CurrentHomeserver == null)
+ {
+ NavigationManager.NavigateTo("/Login");
+ return;
+ }
+ RoomId = RoomId.Replace('~', '.');
+ await LoadStatesAsync();
+ Console.WriteLine("Policy list editor initialized!");
+ }
+
+ private async Task LoadStatesAsync()
+ {
+ using var client = new HttpClient();
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", RuntimeStorage.AccessToken);
+ var response = await client.GetAsync($"{RuntimeStorage.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);
+ PolicyEvents = stateEvents.Where(x => x.type.StartsWith("m.policy.rule"))
+ .Select(x => JsonSerializer.Deserialize<StateEvent<PolicyRuleStateEventData>>(JsonSerializer.Serialize(x))).ToList();
+ StateHasChanged();
+ // foreach (var stateEvent in PolicyEvents.Where(x => x.replaces_state != "" && x.replaces_state != null))
+ // {
+ // Console.WriteLine($"{stateEvent.replaces_state} -> {PolicyEvents.Any(x => x.state_key == stateEvent.replaces_state)}");
+ // }
+ // foreach (var policyEvent in PolicyEvents)
+ // {
+ // Console.WriteLine(policyEvent.ToJson());
+ // }
+ }
+
+}
+
diff --git a/MatrixRoomUtils.Web/Pages/PolicyListRoomList.razor b/MatrixRoomUtils.Web/Pages/PolicyListRoomList.razor
new file mode 100644
index 0000000..39b7087
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/PolicyListRoomList.razor
@@ -0,0 +1,84 @@
+@page "/PolicyListEditor"
+@using MatrixRoomUtils.Authentication
+@using MatrixRoomUtils.Web.Classes
+@using Blazored.LocalStorage
+@using System.Net.Http.Headers
+@using System.Text.Json
+@using MatrixRoomUtils.Extensions
+@using MatrixRoomUtils.StateEventTypes
+@inject ILocalStorageService LocalStorage
+@inject NavigationManager NavigationManager
+<h3>Policy list editor</h3>
+
+<h5>Room list</h5>
+<hr/>
+@if (PolicyRoomList.Count == 0)
+{
+ <p>No policy rooms found.</p>
+}
+else
+{
+ foreach (var s in PolicyRoomList)
+ {
+ <a href="@(NavigationManager.Uri + "/" + s.Replace('.', '~'))">@s</a>
+ <br/>
+ }
+ <div style="margin-bottom: 4em;"></div>
+}
+
+<LogView></LogView>
+
+@code {
+ //get room list
+ // - sync withroom list filter
+ // type = support.feline.msc3784
+ //support.feline.policy.lists.msc.v1
+
+ public List<string> PolicyRoomList { get; set; } = new();
+ public List<StateEvent<PolicyRuleStateEventData>> PolicyEvents { get; set; } = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (!RuntimeStorage.WasLoaded) await RuntimeStorage.LoadFromLocalStorage(LocalStorage);
+ await base.OnInitializedAsync();
+ if(RuntimeStorage.AccessToken == null || RuntimeStorage.CurrentHomeserver == null)
+ {
+ NavigationManager.NavigateTo("/Login");
+ return;
+ }
+ await EnumeratePolicyRooms();
+ Console.WriteLine("Policy list editor initialized!");
+ }
+
+ private async Task EnumeratePolicyRooms()
+ {
+ using HttpClient wc = new();
+ wc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", RuntimeStorage.AccessToken);
+
+ //get room list
+ //temporary hack until rooms get enumerated...
+ string[] rooms = { "!fTjMjIzNKEsFlUIiru:neko.dev" };
+
+ foreach (string room in rooms)
+ {
+ Console.WriteLine($"Checking if {room} is a policy room...");
+ var sk = await wc.GetAsync($"{RuntimeStorage.CurrentHomeserver}/_matrix/client/v3/rooms/{room}/state/org.matrix.mjolnir.shortcode");
+ if (sk.IsSuccessStatusCode)
+ {
+ Console.WriteLine($"Got success...");
+ var sko = await sk.Content.ReadFromJsonAsync<JsonElement>();
+ if (sko.TryGetProperty("shortcode", out JsonElement shortcode))
+ {
+ Console.WriteLine($"Room {room} has a shortcode: {shortcode.GetString()}!");
+ PolicyRoomList.Add(room);
+ StateHasChanged();
+ }
+ else Console.WriteLine("No record found...");
+ }
+ else Console.WriteLine($"Got failure {sk.StatusCode}...");
+ }
+
+ //print to console
+ Console.WriteLine($"Detected policy lists: {PolicyRoomList.ToJson()}");
+ }
+ }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/UserImportPage.razor b/MatrixRoomUtils.Web/Pages/UserImportPage.razor
new file mode 100644
index 0000000..ca0a213
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/UserImportPage.razor
@@ -0,0 +1,74 @@
+@page "/ImportUsers"
+@using MatrixRoomUtils.Authentication
+@using MatrixRoomUtils.Web.Classes
+@using Blazored.LocalStorage
+@using System.Text.Json
+@inject ILocalStorageService LocalStorage
+<h3>Login</h3>
+
+<InputFile OnChange="@FileChanged"></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: @(RuntimeStorage.UsersCache.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(RuntimeStorage.UsersCache.Any(x => x.Value.LoginResponse.UserId == $"@{username}:{homeserver}")) continue;
+ var result = await MatrixAccount.Login(homeserver, username, password);
+ Console.WriteLine($"Obtained access token for {result.UserId}!");
+
+ RuntimeStorage.AccessToken = result.AccessToken;
+
+ var userinfo = new UserInfo()
+ {
+ LoginResponse = result
+ };
+ userinfo.Profile = await MatrixAccount.GetProfile(result.HomeServer, result.UserId);
+
+ RuntimeStorage.UsersCache.Add(result.AccessToken, userinfo);
+ StateHasChanged();
+ }
+
+ await RuntimeStorage.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
|