diff options
29 files changed, 1185 insertions, 39 deletions
diff --git a/ArcaneLibs b/ArcaneLibs -Subproject e9cf8d3061cca5319982c1ef15b78f7c7ca36e8 +Subproject 7c8f845f2376890aeb9d564f078ee93c3157d57 diff --git a/LibMatrix b/LibMatrix -Subproject 0330ff6706a968400ca8fe2a3e3ccf6237a1556 +Subproject 478fc1b0ef855530e1e93c5212d154280f9d7dd diff --git a/MatrixRoomUtils.LibDMSpace/DMSpaceConfiguration.cs b/MatrixRoomUtils.LibDMSpace/DMSpaceConfiguration.cs new file mode 100644 index 0000000..4085367 --- /dev/null +++ b/MatrixRoomUtils.LibDMSpace/DMSpaceConfiguration.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace MatrixRoomUtils.LibDMSpace; + +//gay.rory.dm_space +public class DMSpaceConfiguration { + public const string EventId = "gay.rory.dm_space"; + + [JsonPropertyName("dm_space_id")] + public string? DMSpaceId { get; set; } + +} \ No newline at end of file diff --git a/MatrixRoomUtils.LibDMSpace/DMSpaceRoom.cs b/MatrixRoomUtils.LibDMSpace/DMSpaceRoom.cs new file mode 100644 index 0000000..247ace8 --- /dev/null +++ b/MatrixRoomUtils.LibDMSpace/DMSpaceRoom.cs @@ -0,0 +1,83 @@ +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.Homeservers; +using LibMatrix.RoomTypes; +using MatrixRoomUtils.LibDMSpace.StateEvents; + +namespace MatrixRoomUtils.LibDMSpace; + +public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : SpaceRoom(homeserver, roomId) { + private readonly GenericRoom _room; + + public async Task<DMSpaceInfo?> GetDmSpaceInfo() { + return await GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId); + } + + public async IAsyncEnumerable<GenericRoom> GetChildrenAsync(bool includeRemoved = false) { + var rooms = new List<GenericRoom>(); + var state = GetFullStateAsync(); + await foreach (var stateEvent in state) { + if (stateEvent!.Type != "m.space.child") continue; + if (stateEvent.RawContent!.ToJson() != "{}" || includeRemoved) + yield return homeserver.GetRoom(stateEvent.StateKey); + } + } + + public async Task<EventIdResponse> AddChildAsync(GenericRoom room) { + var members = room.GetMembersAsync(true); + Dictionary<string, int> memberCountByHs = new(); + await foreach (var member in members) { + var server = member.StateKey.Split(':')[1]; + if (memberCountByHs.ContainsKey(server)) memberCountByHs[server]++; + else memberCountByHs[server] = 1; + } + + var resp = await SendStateEventAsync("m.space.child", room.RoomId, new { + via = memberCountByHs + .OrderByDescending(x => x.Value) + .Select(x => x.Key) + .Take(10) + }); + return resp; + } + + public async Task ImportNativeDMs() { + var dmSpaceInfo = await GetDmSpaceInfo(); + if (dmSpaceInfo is null) throw new NullReferenceException("DM Space is not configured!"); + if (dmSpaceInfo.LayerByUser) + await ImportNativeDMsIntoLayers(); + else await ImportNativeDMsWithoutLayers(); + } + +#region Import Native DMs + + private async Task ImportNativeDMsWithoutLayers() { + var mdirect = await homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + foreach (var (userId, dmRooms) in mdirect) { + foreach (var roomid in dmRooms) { + var dri = new DMRoomInfo() { + RemoteUsers = new() { + userId + } + }; + // Add all DM room members + var members = homeserver.GetRoom(roomid).GetMembersAsync(); + await foreach (var member in members) + if (member.StateKey != userId) + dri.RemoteUsers.Add(member.StateKey); + // Remove members of DM space + members = GetMembersAsync(); + await foreach (var member in members) + if (dri.RemoteUsers.Contains(member.StateKey)) + dri.RemoteUsers.Remove(member.StateKey); + await SendStateEventAsync(DMRoomInfo.EventId, roomid, dri); + } + } + } + + private async Task ImportNativeDMsIntoLayers() { + var mdirect = await homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + } + +#endregion +} \ No newline at end of file diff --git a/MatrixRoomUtils.LibDMSpace/MatrixRoomUtils.LibDMSpace.csproj b/MatrixRoomUtils.LibDMSpace/MatrixRoomUtils.LibDMSpace.csproj new file mode 100644 index 0000000..70b4ffc --- /dev/null +++ b/MatrixRoomUtils.LibDMSpace/MatrixRoomUtils.LibDMSpace.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <LangVersion>preview</LangVersion> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Condition="Exists('..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj"/> + <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="ArcaneLibs" Version="*-preview*"/> + <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" /> + <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj"/> + </ItemGroup> +</Project> diff --git a/MatrixRoomUtils.LibDMSpace/StateEvents/DMRoomInfo.cs b/MatrixRoomUtils.LibDMSpace/StateEvents/DMRoomInfo.cs new file mode 100644 index 0000000..b88f06a --- /dev/null +++ b/MatrixRoomUtils.LibDMSpace/StateEvents/DMRoomInfo.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; +using LibMatrix.EventTypes; +using LibMatrix.Interfaces; + +namespace MatrixRoomUtils.LibDMSpace.StateEvents; + +[MatrixEvent(EventName = EventId)] +public class DMRoomInfo : EventContent { + public const string EventId = "gay.rory.dm_room_info"; + [JsonPropertyName("remote_users")] + public List<string> RemoteUsers { get; set; } + + +} \ No newline at end of file diff --git a/MatrixRoomUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs b/MatrixRoomUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs new file mode 100644 index 0000000..7824324 --- /dev/null +++ b/MatrixRoomUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; +using LibMatrix.EventTypes; +using LibMatrix.Interfaces; + +namespace MatrixRoomUtils.LibDMSpace.StateEvents; + +[MatrixEvent(EventName = EventId)] +public class DMSpaceInfo : EventContent { + public const string EventId = "gay.rory.dm_space_info"; + + [JsonPropertyName("is_layered")] + public bool LayerByUser { get; set; } + +} \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs b/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs index 3223ec6..b6836c8 100644 --- a/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs +++ b/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs @@ -7,8 +7,6 @@ namespace MatrixRoomUtils.Web.Classes; public class MRUStorageWrapper(TieredStorageService storageService, HomeserverProviderService homeserverProviderService, NavigationManager navigationManager) { public async Task<List<UserAuth>?> GetAllTokens() { - if (!await storageService.DataStorageProvider.ObjectExistsAsync("mru.tokens")) { } - return await storageService.DataStorageProvider.LoadObjectAsync<List<UserAuth>>("mru.tokens") ?? new List<UserAuth>(); } @@ -48,6 +46,10 @@ public class MRUStorageWrapper(TieredStorageService storageService, HomeserverPr return await homeserverProviderService.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken); } + public async Task<AuthenticatedHomeserverGeneric?> GetSession(UserAuth userAuth) { + return await homeserverProviderService.GetAuthenticatedWithToken(userAuth.Homeserver, userAuth.AccessToken); + } + public async Task<AuthenticatedHomeserverGeneric?> GetCurrentSessionOrNavigate() { AuthenticatedHomeserverGeneric? session = null; diff --git a/MatrixRoomUtils.Web/Classes/RoomInfo.cs b/MatrixRoomUtils.Web/Classes/RoomInfo.cs index 31943dc..a2fa6f5 100644 --- a/MatrixRoomUtils.Web/Classes/RoomInfo.cs +++ b/MatrixRoomUtils.Web/Classes/RoomInfo.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using System.Text.Json.Nodes; using ArcaneLibs; using LibMatrix; using LibMatrix.EventTypes.Spec.State; @@ -8,7 +9,7 @@ using LibMatrix.RoomTypes; namespace MatrixRoomUtils.Web.Classes; public class RoomInfo : NotifyPropertyChanged { - public GenericRoom Room { get; set; } + public GenericRoom? Room { get; set; } public ObservableCollection<StateEventResponse?> StateEvents { get; } = new(); public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") { @@ -19,11 +20,12 @@ public class RoomInfo : NotifyPropertyChanged { Type = type, StateKey = stateKey }; + if (Room is null) return null; try { - @event.TypedContent = await Room.GetStateAsync<EventContent>(type, stateKey); + @event.RawContent = await Room.GetStateAsync<JsonObject>(type, stateKey); } catch (MatrixException e) { - if (e is { ErrorCode: "M_NOT_FOUND" }) @event.TypedContent = default!; + if (e is { ErrorCode: "M_NOT_FOUND" }) @event.RawContent = default!; else throw; } @@ -37,7 +39,7 @@ public class RoomInfo : NotifyPropertyChanged { } public string? RoomName { - get => _roomName ?? Room.RoomId; + get => _roomName ?? DefaultRoomName ?? Room.RoomId; set => SetField(ref _roomName, value); } @@ -58,6 +60,8 @@ public class RoomInfo : NotifyPropertyChanged { private string? _roomName; private RoomCreateEventContent? _creationEventContent; private string? _roomCreator; + + public string? DefaultRoomName { get; set; } public RoomInfo() { StateEvents.CollectionChanged += (_, args) => { diff --git a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj index 625a303..543f8db 100644 --- a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj +++ b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj @@ -21,9 +21,14 @@ <ProjectReference Condition="Exists('..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj" /> <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="ArcaneLibs" Version="*-preview*" /> <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" /> + <ProjectReference Include="..\MatrixRoomUtils.LibDMSpace\MatrixRoomUtils.LibDMSpace.csproj" /> </ItemGroup> + <ItemGroup> + <Folder Include="Classes\AccountData\" /> + </ItemGroup> + diff --git a/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor b/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor index 9b0f61c..a1e928f 100644 --- a/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor +++ b/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor @@ -1,5 +1,8 @@ @page "/Dev/Options" @using ArcaneLibs.Extensions +@using System.Text.Unicode +@using System.Text +@using System.Text.Json @inject NavigationManager NavigationManager @inject ILocalStorageService LocalStorage @@ -8,23 +11,61 @@ <h3>Rory&::MatrixUtils - Developer options</h3> <hr/> -<InputCheckbox @bind-Value="@settings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox><label> Enable log views</label><br/> -<InputCheckbox @bind-Value="@settings.DeveloperSettings.EnableConsoleLogging" @oninput="@LogStuff"></InputCheckbox><label> Enable console logging</label><br/> -<InputCheckbox @bind-Value="@settings.DeveloperSettings.EnablePortableDevtools" @oninput="@LogStuff"></InputCheckbox><label> Enable portable devtools</label><br/> +<p> + <span>Import local storage: </span> + <InputFile OnChange="ImportLocalStorage"></InputFile> +</p> +<p> + <span>Export local storage: </span> + <button @onclick="@ExportLocalStorage">Export</button> +</p> + +@if (userSettings is not null) { + <InputCheckbox @bind-Value="@userSettings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox> + <label> Enable log views</label> + <br/> + <InputCheckbox @bind-Value="@userSettings.DeveloperSettings.EnableConsoleLogging" @oninput="@LogStuff"></InputCheckbox> + <label> Enable console logging</label> + <br/> + <InputCheckbox @bind-Value="@userSettings.DeveloperSettings.EnablePortableDevtools" @oninput="@LogStuff"></InputCheckbox> + <label> Enable portable devtools</label> + <br/> +} <br/> @code { - MRUStorageWrapper.Settings settings { get; set; } = new(); + private MRUStorageWrapper.Settings? userSettings { get; set; } protected override async Task OnInitializedAsync() { - settings = await TieredStorage.DataStorageProvider.LoadObjectAsync<MRUStorageWrapper.Settings>("mru.settings"); + // userSettings = await TieredStorage.DataStorageProvider.LoadObjectAsync<MRUStorageWrapper.Settings>("mru.settings"); + await base.OnInitializedAsync(); } private async Task LogStuff() { await Task.Delay(100); - Console.WriteLine($"Settings: {settings.ToJson()}"); - await TieredStorage.DataStorageProvider.SaveObjectAsync("mru.settings", settings); + Console.WriteLine($"Settings: {userSettings.ToJson()}"); + await TieredStorage.DataStorageProvider.SaveObjectAsync("mru.settings", userSettings); + } + + private async Task ExportLocalStorage() { + var keys = await TieredStorage.DataStorageProvider.GetAllKeysAsync(); + var data = new Dictionary<string, object>(); + foreach (var key in keys) { + data.Add(key, await TieredStorage.DataStorageProvider.LoadObjectAsync<object>(key)); + } + var dataUri = "data:application/json;base64,"; + dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data))); + await JSRuntime.InvokeVoidAsync("window.open", dataUri, "_blank"); + } + + private async Task ImportLocalStorage(InputFileChangeEventArgs obj) { + if (obj.FileCount != 1) return; + var data = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(obj.File.OpenReadStream()); + foreach (var (key, value) in data) { + await TieredStorage.DataStorageProvider.SaveObjectAsync(key, value); + } + NavigationManager.NavigateTo(NavigationManager.Uri, true, true); } } diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor index 4d98402..516e9ca 100644 --- a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor +++ b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor @@ -22,7 +22,7 @@ @code { private ObservableCollection<RoomInfo> Rooms { get; } = new(); - private ProfileResponseEventContent GlobalProfile { get; set; } + private UserProfileResponse GlobalProfile { get; set; } private AuthenticatedHomeserverGeneric? Homeserver { get; set; } diff --git a/MatrixRoomUtils.Web/Pages/Tools/CopyPowerlevel.razor b/MatrixRoomUtils.Web/Pages/Tools/CopyPowerlevel.razor new file mode 100644 index 0000000..aaeb5a3 --- /dev/null +++ b/MatrixRoomUtils.Web/Pages/Tools/CopyPowerlevel.razor @@ -0,0 +1,84 @@ +@page "/CopyPowerlevel" +@using System.Diagnostics +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Homeservers +@using LibMatrix.RoomTypes +<h3>Copy powerlevel</h3> +<hr/> + +<p>Users: </p> +@foreach (var hs in hss) { + <p>@hs.WhoAmI.UserId</p> +} + +<br/> +<LinkButton OnClick="Execute">Execute</LinkButton> +<br/> +@foreach (var line in Enumerable.Reverse(log)) { + <p>@line</p> +} + +@code { + private List<string> log { get; set; } = new(); + List<AuthenticatedHomeserverGeneric> hss { get; set; } = new(); + + protected override async Task OnInitializedAsync() { + var hs = await MRUStorage.GetCurrentSessionOrNavigate(); + if (hs is null) return; + var sessions = await MRUStorage.GetAllTokens(); + foreach (var userAuth in sessions) { + var session = await MRUStorage.GetSession(userAuth); + if (session is not null) { + hss.Add(session); + StateHasChanged(); + } + } + + StateHasChanged(); + Console.WriteLine("Rerendered!"); + await base.OnInitializedAsync(); + } + + private async Task Execute() { + foreach (var hs in hss) { + var rooms = await hs.GetJoinedRooms(); + var tasks = rooms.Select(x=>Execute(hs, x)).ToAsyncEnumerable(); + await foreach (var a in tasks) { + if (!string.IsNullOrWhiteSpace(a)) { + log.Add(a); + StateHasChanged(); + } + } + } + } + + private async Task<string> Execute(AuthenticatedHomeserverGeneric hs, GenericRoom room) { + try { + var pls = await room.GetPowerLevelsAsync(); + // if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) == pls.UsersDefault) return "I am default PL in " + room.RoomId; + if (!pls.UserHasPermission(hs.WhoAmI.UserId, RoomPowerLevelEventContent.EventId)) return "I do not have permission to send PL in " + room.RoomId; + foreach (var ahs in hss) { + if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) == pls.GetUserPowerLevel(ahs.WhoAmI.UserId)) { + log.Add("I am same PL in " + room.RoomId); + continue; + } + + pls.SetUserPowerLevel(ahs.WhoAmI.UserId, pls.GetUserPowerLevel(hs.WhoAmI.UserId)); + await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, pls); + log.Add($"Updated powerlevel of {room.RoomId} to {pls.GetUserPowerLevel(ahs.WhoAmI.UserId)}"); + } + + } + catch (MatrixException e) { + return $"Failed to update PLs in {room.RoomId}: {e.Message}"; + } + catch (Exception e) { + return $"Failed to update PLs in {room.RoomId}: {e.Message}"; + } + StateHasChanged(); + return ""; + } + +} \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/Tools/MassJoinRoom.razor b/MatrixRoomUtils.Web/Pages/Tools/MassJoinRoom.razor new file mode 100644 index 0000000..bcf8095 --- /dev/null +++ b/MatrixRoomUtils.Web/Pages/Tools/MassJoinRoom.razor @@ -0,0 +1,110 @@ +@page "/MassRoomJoin" +@using System.Diagnostics +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Homeservers +@using LibMatrix.RoomTypes +<h3>Mass join room</h3> +<hr/> +<p>Room: </p> +<FancyTextBox @bind-Value="@roomId"></FancyTextBox> + +<p>Users: </p> +@foreach (var hs in hss) { + <p>@hs.WhoAmI.UserId</p> +} + +<br/> +<LinkButton OnClick="Execute">Execute</LinkButton> +<br/> +@foreach (var line in Enumerable.Reverse(log)) { + <p>@line</p> +} + +@code { + private List<string> log { get; set; } = new(); + List<AuthenticatedHomeserverGeneric> hss { get; set; } = new(); + string roomId { get; set; } + + protected override async Task OnInitializedAsync() { + var hs = await MRUStorage.GetCurrentSessionOrNavigate(); + if (hs is null) return; + var sessions = await MRUStorage.GetAllTokens(); + foreach (var userAuth in sessions) { + var session = await MRUStorage.GetSession(userAuth); + if (session is not null) { + hss.Add(session); + StateHasChanged(); + } + } + + StateHasChanged(); + Console.WriteLine("Rerendered!"); + await base.OnInitializedAsync(); + } + + private async Task Execute() { + // foreach (var hs in hss) { + // var rooms = await hs.GetJoinedRooms(); + var tasks = hss.Select(ExecuteInvite).ToAsyncEnumerable(); + await foreach (var a in tasks) { + if (!string.IsNullOrWhiteSpace(a)) { + log.Add(a); + StateHasChanged(); + } + } + tasks = hss.Select(ExecuteJoin).ToAsyncEnumerable(); + await foreach (var a in tasks) { + if (!string.IsNullOrWhiteSpace(a)) { + log.Add(a); + StateHasChanged(); + } + } + // } + } + + private async Task<string> ExecuteInvite(AuthenticatedHomeserverGeneric hs) { + var room = hs.GetRoom(roomId); + try { + try { + var joinRule = await room.GetJoinRuleAsync(); + if (joinRule.JoinRule == "public") return "Room is public, no invite needed"; + } + catch { } + var pls = await room.GetPowerLevelsAsync(); + if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) < pls.Invite) return "I do not have permission to send invite in " + room.RoomId; + await room.InviteUsersAsync(hss.Select(x => x.WhoAmI.UserId).ToList()); + log.Add($"Invited to {room.RoomId} to {pls.GetUserPowerLevel(hs.WhoAmI.UserId)}"); + } + catch (MatrixException e) { + return $"Failed to invite in {room.RoomId}: {e.Message}"; + } + catch (Exception e) { + return $"Failed to invite in {room.RoomId}: {e.Message}"; + } + StateHasChanged(); + return ""; + } + + private async Task<string> ExecuteJoin(AuthenticatedHomeserverGeneric hs) { + var room = hs.GetRoom(roomId); + try { + try { + var mse = await room.GetStateOrNullAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, hs.WhoAmI.UserId); + if (mse?.Membership == "join") return $"User {hs.WhoAmI.UserId} already in room"; + } + catch { } + await room.JoinAsync(); + } + catch (MatrixException e) { + return $"Failed to join {hs.WhoAmI.UserId} to {room.RoomId}: {e.Message}"; + } + catch (Exception e) { + return $"Failed to join {hs.WhoAmI.UserId} to {room.RoomId}: {e.Message}"; + } + StateHasChanged(); + return ""; + } + +} \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/User/DMManager.razor b/MatrixRoomUtils.Web/Pages/User/DMManager.razor new file mode 100644 index 0000000..04ff6e5 --- /dev/null +++ b/MatrixRoomUtils.Web/Pages/User/DMManager.razor @@ -0,0 +1,56 @@ +@page "/User/DirectMessages" +@using LibMatrix.Homeservers +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +<h3>Direct Messages</h3> +<hr/> + +@foreach (var (targetUser, rooms) in DMRooms) { + <div> + <InlineUserItem User="targetUser"></InlineUserItem> + @foreach (var room in rooms) { + <RoomListItem RoomInfo="room" LoadData="true"></RoomListItem> + } + </div> +} + +@code { + private string? _status; + private AuthenticatedHomeserverGeneric? HomeServer { get; set; } + private Dictionary<UserProfileResponse, List<RoomInfo>> DMRooms { get; set; } = new(); + + public string? Status { + get => _status; + set { + _status = value; + StateHasChanged(); + } + } + + protected override async Task OnInitializedAsync() { + HomeServer = await MRUStorage.GetCurrentSessionOrNavigate(); + if (HomeServer is null) return; + Status = "Loading global profile..."; + if (HomeServer.WhoAmI?.UserId is null) return; + + Status = "Loading DM list from account data..."; + var dms = await HomeServer.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + DMRooms.Clear(); + foreach (var (userId, rooms) in dms) { + var roomList = new List<RoomInfo>(); + DMRooms.Add(await HomeServer.GetProfileAsync(userId), roomList); + foreach (var room in rooms) { + roomList.Add(new RoomInfo() { Room = HomeServer.GetRoom(room) }); + } + StateHasChanged(); + } + + StateHasChanged(); + Status = null; + + await base.OnInitializedAsync(); + } + + + +} \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpace.razor b/MatrixRoomUtils.Web/Pages/User/DMSpace.razor new file mode 100644 index 0000000..21cc264 --- /dev/null +++ b/MatrixRoomUtils.Web/Pages/User/DMSpace.razor @@ -0,0 +1,86 @@ +@page "/User/DMSpace/Setup" +@using LibMatrix.Homeservers +@using LibMatrix +@using MatrixRoomUtils.LibDMSpace +@using MatrixRoomUtils.LibDMSpace.StateEvents +@using MatrixRoomUtils.Web.Pages.User.DMSpaceStages +<h3>DM Space Management</h3> +<hr/> +<CascadingValue Value="@DmSpace"> + @switch (Stage) { + case -1: + <p>Initialising...</p> + break; + case 0: + <DMSpaceStage0/> + break; + case 1: + <DMSpaceStage1/> + break; + case 2: + <DMSpaceStage2/> + break; + case 3: + <DMSpaceStage3/> + break; + default: + <p>Stage is unknown value: @Stage!</p> + break; + } +</CascadingValue> + +@code { + private int _stage = -1; + + [Parameter, SupplyParameterFromQuery(Name = "stage")] + public int Stage { + get => _stage; + set { + _stage = value; + Console.WriteLine($"Stage is now {value}"); + StateHasChanged(); + } + } + + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + public DMSpaceConfiguration? DmSpaceConfiguration { get; set; } + + [Parameter] + public DMSpace? DmSpace { get; set; } + + protected override async Task OnInitializedAsync() { + if (NavigationManager.Uri.Contains("?stage=")) { + NavigationManager.NavigateTo("/User/DMSpace", true); + } + DmSpace = this; + Homeserver ??= await MRUStorage.GetCurrentSessionOrNavigate(); + if (Homeserver is null) return; + try { + DmSpaceConfiguration = await Homeserver.GetAccountDataAsync<DMSpaceConfiguration>("gay.rory.dm_space"); + var room = Homeserver.GetRoom(DmSpaceConfiguration.DMSpaceId); + await room.GetStateAsync<object>(DMSpaceInfo.EventId); + Stage = 1; + } + catch (MatrixException e) { + if (e.ErrorCode == "M_NOT_FOUND") { + Stage = 0; + DmSpaceConfiguration = new(); + } + else throw; + } + catch (Exception e) { + throw; + } + finally { + StateHasChanged(); + } + await base.OnInitializedAsync(); + } + + protected override async Task OnParametersSetAsync() { + StateHasChanged(); + await base.OnParametersSetAsync(); + } + +} \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor new file mode 100644 index 0000000..49fd5b4 --- /dev/null +++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor @@ -0,0 +1,11 @@ +<b> + <u>Welcome to the DM Space tool!</u> +</b> +<p>This wizard will help you set up a DM space.</p> +<p>This is useful for eg. sharing DM rooms across multiple accounts.</p> +<br/> +<LinkButton href="/User/DMSpace?stage=1">Get started</LinkButton> + +@code { + +} \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor new file mode 100644 index 0000000..3642da5 --- /dev/null +++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor @@ -0,0 +1,128 @@ +@using LibMatrix.Homeservers +@using LibMatrix.RoomTypes +@using LibMatrix +@using LibMatrix.Responses +@using MatrixRoomUtils.LibDMSpace +@using MatrixRoomUtils.LibDMSpace.StateEvents +@using Microsoft.Extensions.Primitives +@using ArcaneLibs.Extensions +<b> + <u>DM Space setup tool - stage 1: Configure space</u> +</b> +<p>You will need a space to use for DM rooms.</p> +@if (DmSpace is not null) { + <p> + Selected space: + <InputSelect @bind-Value="DmSpace.DmSpaceConfiguration.DMSpaceId"> + @foreach (var (id, name) in spaces) { + <option value="@id">@name</option> + } + </InputSelect> + </p> + <p> + <InputCheckbox @bind-Value="DmSpaceInfo.LayerByUser"></InputCheckbox> + Create sub-spaces per user + </p> +} +else { + <b>Error: DmSpaceConfiguration is null!</b> +} + +<br/> +<LinkButton OnClick="@Execute">Next</LinkButton> + +@if (!string.IsNullOrWhiteSpace(Status)) { + <p>@Status</p> +} + +@code { + + private string? Status { + get => _status; + set { + _status = value; + StateHasChanged(); + } + } + + private Dictionary<string, string> spaces = new() { { "", "New space" } }; + private string? _status; + + [CascadingParameter] + public DMSpace? DmSpace { get; set; } + + public DMSpaceInfo? DmSpaceInfo { get; set; } = new(); + + protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); + } + + SemaphoreSlim _semaphoreSlim = new(1, 1); + protected override async Task OnParametersSetAsync() { + if (DmSpace is null) + return; + await _semaphoreSlim.WaitAsync(); + DmSpace.DmSpaceConfiguration ??= new(); + if (spaces.Count == 1) { + Status = "Looking for spaces..."; + var userRoomsEnum = DmSpace.Homeserver.GetJoinedRoomsByType("m.space"); + List<GenericRoom> userRooms = new(); + await foreach (var room in userRoomsEnum) { + userRooms.Add(room); + } + var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncEnumerable(); + await foreach(var room in roomChecks) + if(room.HasValue) + spaces.TryAdd(room.Value.id, room.Value.name); + + Status = "Done!"; + } + _semaphoreSlim.Release(); + await base.OnParametersSetAsync(); + } + + private async Task Execute() { + if (string.IsNullOrWhiteSpace(DmSpace.DmSpaceConfiguration.DMSpaceId)) { + var crr = CreateRoomRequest.CreatePrivate(DmSpace.Homeserver, "Direct Messages"); + crr._creationContentBaseType.Type = "m.space"; + DmSpace.DmSpaceConfiguration.DMSpaceId = (await DmSpace.Homeserver.CreateRoom(crr)).RoomId; + } + await DmSpace.Homeserver!.SetAccountDataAsync(DMSpaceConfiguration.EventId, DmSpace.DmSpaceConfiguration); + var space = DmSpace.Homeserver.GetRoom(DmSpace.DmSpaceConfiguration.DMSpaceId); + await space.SendStateEventAsync(DMSpaceInfo.EventId, DmSpaceInfo); + + NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=2"); + } + + public async Task<(string id, string name)?> GetFeasibleSpaces(GenericRoom room) { + try { + var pls = await room.GetPowerLevelsAsync(); + if (!pls.UserHasPermission(DmSpace.Homeserver.WhoAmI.UserId, "m.space.child")) { + Console.WriteLine($"No permission to send m.space.child in {room.RoomId}..."); + return null; + } + var roomName = await room.GetNameAsync(); + Status = $"Found viable space: {roomName}"; + if (string.IsNullOrWhiteSpace(DmSpace.DmSpaceConfiguration.DMSpaceId)) { + try { + var dsi = await DmSpace.Homeserver.GetRoom(room.RoomId).GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) ?? new DMSpaceInfo(); + if (await room.GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) is not null && dsi is not null) { + DmSpace.DmSpaceConfiguration.DMSpaceId = room.RoomId; + DmSpaceInfo = dsi; + } + } + catch (MatrixException e) { + if (e.ErrorCode == "M_NOT_FOUND") Console.WriteLine($"{room.RoomId} is not a DM space."); + else throw; + } + } + return (room.RoomId, roomName); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_NOT_FOUND") Console.WriteLine($"m.room.power_levels does not exist in {room.RoomId}!!!"); + else throw; + } + return null; + } + +} \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor new file mode 100644 index 0000000..553f46d --- /dev/null +++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor @@ -0,0 +1,241 @@ +@using LibMatrix.Homeservers +@using LibMatrix.RoomTypes +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +@using MatrixRoomUtils.LibDMSpace +@using MatrixRoomUtils.LibDMSpace.StateEvents +@using ArcaneLibs.Extensions +@using System.Text.Json.Serialization +<b> + <u>DM Space setup tool - stage 2: Fix DM room attribution</u> +</b> +<p>This is just to make sure that your DMs are attributed to the right person!</p> + +@if (!string.IsNullOrWhiteSpace(Status)) { + <p>@Status</p> +} + +@if (DmSpace is not null) { + @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.Id)) { + <InlineUserItem User="@userId"></InlineUserItem> + @foreach (var roomInfo in room) { + <RoomListItem RoomInfo="@roomInfo"> + <LinkButton Round="true" OnClick="@(async () => DmToReassign = roomInfo)">Reassign</LinkButton> + </RoomListItem> + } + } +} +else { + <b>Error: DmSpaceConfiguration is null!</b> +} + +<br/> +<LinkButton OnClick="@Execute">Next</LinkButton> + +@{ + var _offset = 0; +} +@foreach (var (room, usersList) in duplicateDmRooms) { + <ModalWindow Title="Duplicate room found" X="_offset += 30" Y="_offset"> + <p>Found room assigned to multiple users: <RoomListItem RoomInfo="@room"></RoomListItem></p> + <p>Users:</p> + @foreach (var userProfileResponse in usersList) { + <LinkButton OnClick="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))"> + <span>Assign to </span> + <InlineUserItem User="userProfileResponse"></InlineUserItem> + </LinkButton> + <br/> + } + </ModalWindow> +} + +@if (DmToReassign is not null) { + <ModalWindow Title="Re-assign DM" OnCloseClicked="@(() => DmToReassign = null)"> + <RoomListItem RoomInfo="@DmToReassign"></RoomListItem> + @foreach (var userProfileResponse in roomMembers[DmToReassign]) { + <LinkButton OnClick="@(() => SetRoomAssignment(DmToReassign.Room.RoomId, userProfileResponse.Id))"> + <span>Assign to </span> + <InlineUserItem User="userProfileResponse"></InlineUserItem> + </LinkButton> + <br/> + } + </ModalWindow> +} + +@code { + + private string newMxid { get; set; } = ""; + + private RoomInfo? DmToReassign { + get => _dmToReassign; + set { + _dmToReassign = value; + StateHasChanged(); + } + } + + private string? Status { + get => _status; + set { + _status = value; + StateHasChanged(); + } + } + + private string? _status; + private RoomInfo? _dmToReassign; + + [CascadingParameter] + public DMSpace? DmSpace { get; set; } + + private Dictionary<UserProfileWithId, List<RoomInfo>> dmRooms { get; set; } = new(); + private Dictionary<RoomInfo, List<UserProfileWithId>> duplicateDmRooms { get; set; } = new(); + private Dictionary<RoomInfo, List<UserProfileWithId>> roomMembers { get; set; } = new(); + + protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); + } + + SemaphoreSlim _semaphore = new(1, 1); + + protected override async Task OnParametersSetAsync() { + if (DmSpace is null) + return; + await _semaphore.WaitAsync(); + DmToReassign = null; + var hs = DmSpace.Homeserver; + Status = "Loading DM list from account data..."; + var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + Status = "Optimising DM list from account data..."; + var joinedRooms = (await hs.GetJoinedRooms()).Select(x => x.RoomId).ToList(); + foreach (var (user, rooms) in dms) { + for (var i = rooms.Count - 1; i >= 0; i--) { + var roomId = rooms[i]; + if (!joinedRooms.Contains(roomId)) + rooms.RemoveAt(i); + } + dms[user] = rooms.Distinct().ToList(); + } + dms.RemoveAll((x, y) => y is {Count: 0}); + await DmSpace.Homeserver.SetAccountDataAsync("m.direct", dms); + dmRooms.Clear(); + + Status = "DM list optimised, fetching info..."; + var results = dms.Select(async x => { + var (userId, rooms) = x; + UserProfileWithId userProfile; + try { + var profile = await DmSpace.Homeserver.GetProfileAsync(userId); + userProfile = new() { + AvatarUrl = profile.AvatarUrl, + Id = userId, + DisplayName = profile.DisplayName + }; + } + catch { + userProfile = new() { + AvatarUrl = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK", + DisplayName = userId, + Id = userId + }; + } + var roomList = new List<RoomInfo>(); + var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable(); + await foreach (var result in tasks) + roomList.Add(result); + return (userProfile, roomList); + // StateHasChanged(); + }).ToAsyncEnumerable(); + await foreach (var res in results) { + dmRooms.Add(res.userProfile, res.roomList); + // Status = $"Listed {dmRooms.Count} users"; + } + _semaphore.Release(); + var duplicateDmRoomIds = new Dictionary<string, List<UserProfileWithId>>(); + foreach (var (user, rooms) in dmRooms) { + foreach (var roomInfo in rooms) { + if (!duplicateDmRoomIds.ContainsKey(roomInfo.Room.RoomId)) + duplicateDmRoomIds.Add(roomInfo.Room.RoomId, new()); + duplicateDmRoomIds[roomInfo.Room.RoomId].Add(user); + } + } + duplicateDmRoomIds.RemoveAll((x, y) => y.Count == 1); + foreach (var (roomId, users) in duplicateDmRoomIds) { + duplicateDmRooms.Add(dmRooms.First(x => x.Value.Any(x => x.Room.RoomId == roomId)).Value.First(x => x.Room.RoomId == roomId), users); + } + + // StateHasChanged(); + Status = null; + await base.OnParametersSetAsync(); + } + + private async Task Execute() { + NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=3"); + } + + private async Task<RoomInfo> GetRoomInfo(GenericRoom room) { + var roomInfo = new RoomInfo() { + Room = room + }; + roomMembers[roomInfo] = new(); + roomInfo.CreationEventContent = await room.GetCreateEventAsync(); + try { + roomInfo.RoomName = await room.GetNameAsync(); + } + catch { } + + var membersEnum = room.GetMembersAsync(); + await foreach (var member in membersEnum) + if (member.TypedContent is RoomMemberEventContent memberEvent && !string.IsNullOrWhiteSpace(memberEvent.Membership) && memberEvent.Membership == "join") + roomMembers[roomInfo].Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey }); + + if (string.IsNullOrWhiteSpace(roomInfo.RoomName) || roomInfo.RoomName == room.RoomId) { + List<string> displayNames = new List<string>(); + foreach (var member in roomMembers[roomInfo]) + if (!string.IsNullOrWhiteSpace(member.DisplayName)) + displayNames.Add(member.DisplayName); + roomInfo.RoomName = string.Join(", ", displayNames); + } + try { + string? roomIcon = (await room.GetAvatarUrlAsync())?.Url; + if (room is not null) + roomInfo.RoomIcon = roomIcon; + } + catch { } + return roomInfo; + } + + private async Task<List<RoomInfo>> GetRoomInfoForRooms(List<GenericRoom> rooms) { + var tasks = rooms.Select(GetRoomInfo).ToList(); + await Task.WhenAll(tasks); + return tasks.Select(x => x.Result).ToList(); + } + + private async Task SetRoomAssignment(string roomId, string userId) { + var hs = DmSpace.Homeserver; + Status = "Loading DM list from account data..."; + var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + Status = "Updating DM list from account data..."; + + foreach (var (user, rooms) in dms) { + rooms.RemoveAll(x => x == roomId); + dms[user] = rooms.Distinct().ToList(); + } + if(!dms.ContainsKey(userId)) + dms.Add(userId, new()); + dms[userId].Add(roomId); + dms.RemoveAll((x, y) => y is {Count: 0}); + await DmSpace.Homeserver.SetAccountDataAsync("m.direct", dms); + + duplicateDmRooms.RemoveAll((x, y) => x.Room.RoomId == roomId); + StateHasChanged(); + if (duplicateDmRooms.Count == 0) await OnParametersSetAsync(); + } + + private class UserProfileWithId : UserProfileResponse { + [JsonIgnore] + public string Id { get; set; } + } + +} \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor new file mode 100644 index 0000000..854b09c --- /dev/null +++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor @@ -0,0 +1,189 @@ +@using LibMatrix.Homeservers +@using LibMatrix.RoomTypes +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +@using MatrixRoomUtils.LibDMSpace +@using MatrixRoomUtils.LibDMSpace.StateEvents +@using ArcaneLibs.Extensions +@using System.Text.Json.Serialization +<b> + <u>DM Space setup tool - stage 3: Preview space layout</u> +</b> +<p>This gives you a preview of how your settings would impact layout!</p> + +@if (!string.IsNullOrWhiteSpace(Status)) { + <p>@Status</p> +} + +@if (DmSpace is not null) { + @if (dmSpaceInfo is not null && dmSpaceRoomInfo is not null) { + <p> + <InputCheckbox @bind-Value="dmSpaceInfo.LayerByUser"></InputCheckbox> + Create sub-spaces per user + </p> + @if (!dmSpaceInfo.LayerByUser) { + <RoomListItem RoomInfo="@dmSpaceRoomInfo"></RoomListItem> + @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.RoomName)) { + @foreach (var roomInfo in room) { + <div style="margin-left: 32px;"> + <RoomListItem RoomInfo="@roomInfo"></RoomListItem> + </div> + } + } + } + else { + <RoomListItem RoomInfo="@dmSpaceRoomInfo"></RoomListItem> + @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.RoomName)) { + <div style="margin-left: 32px;"> + <RoomListItem RoomInfo="@userId"></RoomListItem> + </div> + @foreach (var roomInfo in room) { + <div style="margin-left: 64px;"> + <RoomListItem RoomInfo="@roomInfo"></RoomListItem> + </div> + } + } + } + } + else { + <b>Error: dmSpaceInfo is null!</b> + } +} +else { + <b>Error: DmSpaceConfiguration is null!</b> +} + +<br/> +<LinkButton OnClick="@Execute">Next</LinkButton> + +@code { + + private string? Status { + get => _status; + set { + _status = value; + StateHasChanged(); + } + } + + private string? _status; + + [CascadingParameter] + public DMSpace? DmSpace { get; set; } + + private Dictionary<RoomInfo, List<RoomInfo>> dmRooms { get; set; } = new(); + private DMSpaceInfo? dmSpaceInfo { get; set; } + private RoomInfo? dmSpaceRoomInfo { get; set; } + + protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); + } + + SemaphoreSlim _semaphore = new(1, 1); + + protected override async Task OnParametersSetAsync() { + if (DmSpace is null) + return; + await _semaphore.WaitAsync(); + var hs = DmSpace.Homeserver; + var dmSpaceRoom = new DMSpaceRoom(hs, DmSpace.DmSpaceConfiguration.DMSpaceId); + dmSpaceRoomInfo = new() { + RoomName = await dmSpaceRoom.GetNameAsync(), + CreationEventContent = await dmSpaceRoom.GetCreateEventAsync(), + RoomIcon = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK", + Room = dmSpaceRoom + }; + dmSpaceInfo = await dmSpaceRoom.GetDmSpaceInfo(); + Status = "Loading DM list from account data..."; + var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + dmRooms.Clear(); + + Status = "DM list optimised, fetching info..."; + var results = dms.Select(async x => { + var (userId, rooms) = x; + UserProfileWithId userProfile; + try { + var profile = await DmSpace.Homeserver.GetProfileAsync(userId); + userProfile = new() { + AvatarUrl = profile.AvatarUrl, + Id = userId, + DisplayName = profile.DisplayName + }; + } + catch { + userProfile = new() { + AvatarUrl = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK", + DisplayName = userId, + Id = userId + }; + } + var roomList = new List<RoomInfo>(); + var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable(); + await foreach (var result in tasks) + roomList.Add(result); + return (userProfile, roomList); + }).ToAsyncEnumerable(); + await foreach (var res in results) { + dmRooms.Add(new RoomInfo() { + Room = dmSpaceRoom, + RoomIcon = res.userProfile.AvatarUrl, + RoomName = res.userProfile.DisplayName, + CreationEventContent = await dmSpaceRoom.GetCreateEventAsync() + }, res.roomList); + } + _semaphore.Release(); + Status = null; + await base.OnParametersSetAsync(); + } + + private async Task Execute() { + var hs = DmSpace.Homeserver; + var dmSpaceRoom = new DMSpaceRoom(hs, DmSpace.DmSpaceConfiguration.DMSpaceId); + NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=3"); + } + + private async Task<RoomInfo> GetRoomInfo(GenericRoom room) { + var roomInfo = new RoomInfo() { + Room = room + }; + var roomMembers = new List<UserProfileWithId>(); + roomInfo.CreationEventContent = await room.GetCreateEventAsync(); + try { + roomInfo.RoomName = await room.GetNameAsync(); + } + catch { } + + var membersEnum = room.GetMembersAsync(); + await foreach (var member in membersEnum) + if (member.TypedContent is RoomMemberEventContent memberEvent && !string.IsNullOrWhiteSpace(memberEvent.Membership) && memberEvent.Membership == "join") + roomMembers.Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey }); + + if (string.IsNullOrWhiteSpace(roomInfo.RoomName) || roomInfo.RoomName == room.RoomId) { + List<string> displayNames = new List<string>(); + foreach (var member in roomMembers) + if (!string.IsNullOrWhiteSpace(member.DisplayName)) + displayNames.Add(member.DisplayName); + roomInfo.RoomName = string.Join(", ", displayNames); + } + try { + string? roomIcon = (await room.GetAvatarUrlAsync())?.Url; + if (room is not null) + roomInfo.RoomIcon = roomIcon; + } + catch { } + return roomInfo; + } + + private async Task<List<RoomInfo>> GetRoomInfoForRooms(List<GenericRoom> rooms) { + var tasks = rooms.Select(GetRoomInfo).ToList(); + await Task.WhenAll(tasks); + return tasks.Select(x => x.Result).ToList(); + } + + private class UserProfileWithId : UserProfileResponse { + [JsonIgnore] + public string Id { get; set; } + } + +} \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Pages/User/Manage.razor b/MatrixRoomUtils.Web/Pages/User/Manage.razor index 25debf7..eac47e8 100644 --- a/MatrixRoomUtils.Web/Pages/User/Manage.razor +++ b/MatrixRoomUtils.Web/Pages/User/Manage.razor @@ -1,8 +1,9 @@ -@page "/User/Manage" +@page "/User/Profile" @using LibMatrix.Homeservers @using LibMatrix.EventTypes.Spec.State @using ArcaneLibs.Extensions -<h3>Manage user - @HomeServer?.WhoAmI?.UserId</h3> +@using LibMatrix.Responses +<h3>Manage Profile - @HomeServer?.WhoAmI?.UserId</h3> <hr/> @if (Profile is not null) { @@ -48,8 +49,8 @@ private string? _status = null; private AuthenticatedHomeserverGeneric? HomeServer { get; set; } - private ProfileResponseEventContent? Profile { get; set; } - private ProfileResponseEventContent? OldProfile { get; set; } + private UserProfileResponse? Profile { get; set; } + private UserProfileResponse? OldProfile { get; set; } private string? Status { get => _status; @@ -77,7 +78,7 @@ var roomNameTasks = RoomProfiles.Keys.Select(x => HomeServer.GetRoom(x)).Select(async x => { var name = await x.GetNameAsync(); - return new KeyValuePair<string, string>(x.RoomId, name); + return new KeyValuePair<string, string?>(x.RoomId, name); }).ToAsyncEnumerable(); await foreach (var (roomId, roomName) in roomNameTasks) { // Status = $"Got room name for {roomId}: {roomName}"; diff --git a/MatrixRoomUtils.Web/Shared/InlineUserItem.razor b/MatrixRoomUtils.Web/Shared/InlineUserItem.razor index 3aea0e0..7bc88e5 100644 --- a/MatrixRoomUtils.Web/Shared/InlineUserItem.razor +++ b/MatrixRoomUtils.Web/Shared/InlineUserItem.razor @@ -2,6 +2,7 @@ @using LibMatrix.EventTypes.Spec.State @using LibMatrix.Helpers @using LibMatrix.Homeservers +@using LibMatrix.Responses <div style="background-color: #ffffff11; border-radius: 0.5em; height: 1em; display: inline-block; vertical-align: middle;" alt="@UserId"> <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "vertical-align: top;") width: 1em; height: 1em; border-radius: 50%;" src="@ProfileAvatar"/> <span style="position: relative; top: -5px;">@ProfileName</span> @@ -20,10 +21,10 @@ public RenderFragment? ChildContent { get; set; } [Parameter] - public ProfileResponseEventContent User { get; set; } + public UserProfileResponse? User { get; set; } [Parameter] - public ProfileResponseEventContent MemberEvent { get; set; } + public RoomMemberEventContent? MemberEvent { get; set; } [Parameter] public string? UserId { get; set; } @@ -50,7 +51,7 @@ throw new ArgumentNullException(nameof(UserId)); if (MemberEvent != null) { - User = new ProfileResponseEventContent { + User = new UserProfileResponse { AvatarUrl = MemberEvent.AvatarUrl, DisplayName = MemberEvent.DisplayName }; @@ -61,7 +62,7 @@ } - ProfileAvatar ??= await hsResolver.ResolveMediaUri(HomeServer.ServerName, User.AvatarUrl); + ProfileAvatar ??= HomeServer.ResolveMediaUri(User.AvatarUrl); ProfileName ??= User.DisplayName; _semaphoreSlim.Release(); diff --git a/MatrixRoomUtils.Web/Shared/NavMenu.razor b/MatrixRoomUtils.Web/Shared/NavMenu.razor index 68b491d..daa4a52 100644 --- a/MatrixRoomUtils.Web/Shared/NavMenu.razor +++ b/MatrixRoomUtils.Web/Shared/NavMenu.razor @@ -35,8 +35,14 @@ </div> <div class="nav-item px-3"> - <NavLink class="nav-link" href="User/Manage"> - <span class="oi oi-plus" aria-hidden="true"></span> Manage user + <NavLink class="nav-link" href="User/Profile"> + <span class="oi oi-plus" aria-hidden="true"></span> Manage profile + </NavLink> + </div> + + <div class="nav-item px-3"> + <NavLink class="nav-link" href="User/DirectMessages"> + <span class="oi oi-plus" aria-hidden="true"></span> Manage DMs </NavLink> </div> diff --git a/MatrixRoomUtils.Web/Shared/RoomList.razor b/MatrixRoomUtils.Web/Shared/RoomList.razor index 705f68c..f78c7f7 100644 --- a/MatrixRoomUtils.Web/Shared/RoomList.razor +++ b/MatrixRoomUtils.Web/Shared/RoomList.razor @@ -4,6 +4,7 @@ @using ArcaneLibs.Extensions @using LibMatrix.EventTypes.Spec.State @using System.Collections.ObjectModel +@using LibMatrix.Responses @using _Imports = MatrixRoomUtils.Web._Imports @if (!StillFetching) { <p>Fetching room details... @RoomsWithTypes.Sum(x => x.Value.Count) out of @Rooms.Count done!</p> @@ -23,7 +24,7 @@ else { public ObservableCollection<RoomInfo> Rooms { get; set; } [Parameter] - public ProfileResponseEventContent? GlobalProfile { get; set; } + public UserProfileResponse? GlobalProfile { get; set; } [Parameter] public bool StillFetching { get; set; } = true; diff --git a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor index 27084cc..e2c1285 100644 --- a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor +++ b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor @@ -2,6 +2,7 @@ @using LibMatrix @using LibMatrix.EventTypes.Spec.State @using LibMatrix.Homeservers +@using LibMatrix.Responses <details> <summary>@roomType (@rooms.Count)</summary> @foreach (var room in rooms) { @@ -29,7 +30,7 @@ public KeyValuePair<string, List<RoomInfo>> Category { get; set; } [Parameter] - public ProfileResponseEventContent? GlobalProfile { get; set; } + public UserProfileResponse? GlobalProfile { get; set; } [CascadingParameter] public AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; diff --git a/MatrixRoomUtils.Web/Shared/RoomListItem.razor b/MatrixRoomUtils.Web/Shared/RoomListItem.razor index 79c7f4e..a24ccad 100644 --- a/MatrixRoomUtils.Web/Shared/RoomListItem.razor +++ b/MatrixRoomUtils.Web/Shared/RoomListItem.razor @@ -3,24 +3,25 @@ @using LibMatrix.EventTypes.Spec.State @using LibMatrix.Helpers @using LibMatrix.Homeservers +@using LibMatrix.Responses @using LibMatrix.RoomTypes @using MatrixRoomUtils.Web.Classes.Constants @if (RoomInfo is not null) { <div class="roomListItem @(HasDangerousRoomVersion ? "dangerousRoomVersion" : HasOldRoomVersion ? "oldRoomVersion" : "")" id="@RoomInfo.Room.RoomId"> @if (OwnMemberState != null) { - <img class="avatar32 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "") " @*@(ChildContent is not null ? "vcenter" : "")*@ + <img class="avatar32 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "") @(ChildContent is not null ? "vcenter" : "")" src="@(hs.ResolveMediaUri(OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl) ?? "/icon-192.png")"/> <span class="centerVertical border75 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "")"> @(OwnMemberState?.DisplayName ?? GlobalProfile?.DisplayName ?? "Loading...") </span> <span class="centerVertical noLeftPadding">-></span> } - <img class="avatar32" src="@hs?.ResolveMediaUri(RoomInfo.RoomIcon)"/> @* style="@(ChildContent is not null ? "vertical-align: baseline;" : "")"*@ + <img class="avatar32" src="@hs?.ResolveMediaUri(RoomInfo.RoomIcon)" style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/> <div class="inlineBlock"> <span class="centerVertical">@RoomInfo.RoomName</span> - @* @if (ChildContent is not null) { *@ - @* @ChildContent *@ - @* } *@ + @if (ChildContent is not null) { + @ChildContent + } </div> </div> @@ -31,8 +32,8 @@ else { @code { - // [Parameter] - // public RenderFragment? ChildContent { get; set; } + [Parameter] + public RenderFragment? ChildContent { get; set; } [Parameter] public RoomInfo? RoomInfo { get; set; } @@ -44,7 +45,10 @@ else { public RoomMemberEventContent? OwnMemberState { get; set; } [CascadingParameter] - public ProfileResponseEventContent? GlobalProfile { get; set; } + public UserProfileResponse? GlobalProfile { get; set; } + + [Parameter] + public bool LoadData { get; set; } = false; private bool HasOldRoomVersion { get; set; } = false; private bool HasDangerousRoomVersion { get; set; } = false; @@ -57,6 +61,33 @@ else { Console.WriteLine(a.PropertyName); StateHasChanged(); }; + + if (LoadData) { + try { + await RoomInfo.GetStateEvent("m.room.create"); + if (ShowOwnProfile) + OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.WhoAmI.UserId)).TypedContent as RoomMemberEventContent; + + await RoomInfo.GetStateEvent("m.room.name"); + await RoomInfo.GetStateEvent("m.room.avatar"); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_FORBIDDEN") { + LoadData = false; + RoomInfo.StateEvents.Add(new() { + Type = "m.room.create", + TypedContent = new RoomCreateEventContent() { RoomVersion = "0" } + }); + RoomInfo.StateEvents.Add(new() { + Type = "m.room.name", + TypedContent = new RoomNameEventContent() { + Name = "M_FORBIDDEN: Are you a member of this room? " + RoomInfo.Room.RoomId + } + }); + } + } + } + await base.OnParametersSetAsync(); } @@ -69,7 +100,7 @@ else { if (hs is null) return; try { - await CheckRoomVersion(); + await CheckRoomVersion(); // await GetRoomInfo(); // await LoadOwnProfile(); } @@ -139,4 +170,5 @@ else { // } // } -} \ No newline at end of file +} + diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor index a454103..075e402 100644 --- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor +++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor @@ -1,5 +1,6 @@ @using ArcaneLibs.Extensions @using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses @inherits BaseTimelineItem @if (roomMemberData is not null) { @@ -14,7 +15,7 @@ <i>@Event.StateKey changed their display name to @(roomMemberData.DisplayName ?? Event.Sender)</i> break; case "join": - <i><InlineUserItem User="@(new ProfileResponseEventContent())" HomeServer="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> joined</i> + <i><InlineUserItem User="@(new UserProfileResponse())" HomeServer="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> joined</i> break; case "leave": <i>@Event.StateKey left</i> diff --git a/MatrixRoomUtils.Web/Shared/UserListItem.razor b/MatrixRoomUtils.Web/Shared/UserListItem.razor index 96e8e64..167809e 100644 --- a/MatrixRoomUtils.Web/Shared/UserListItem.razor +++ b/MatrixRoomUtils.Web/Shared/UserListItem.razor @@ -1,6 +1,7 @@ @using LibMatrix.Helpers @using LibMatrix.EventTypes.Spec.State @using LibMatrix.Homeservers +@using LibMatrix.Responses <div style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-Content;"> <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%;" src="@(string.IsNullOrWhiteSpace(User?.AvatarUrl) ? "https://api.dicebear.com/6.x/identicon/svg?seed=" + UserId : User.AvatarUrl)"/> <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px;">@User?.DisplayName</span> @@ -19,7 +20,7 @@ public RenderFragment? ChildContent { get; set; } [Parameter] - public ProfileResponseEventContent? User { get; set; } + public UserProfileResponse? User { get; set; } [Parameter] public string UserId { get; set; } diff --git a/MatrixRoomUtils.sln b/MatrixRoomUtils.sln index 79650cf..3ea06bf 100755 --- a/MatrixRoomUtils.sln +++ b/MatrixRoomUtils.sln @@ -44,6 +44,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "LibMatri EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestDataGenerator", "LibMatrix\Tests\TestDataGenerator\TestDataGenerator.csproj", "{F3312DE9-4335-4E85-A4CF-2616427A651E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixRoomUtils.LibDMSpace", "MatrixRoomUtils.LibDMSpace\MatrixRoomUtils.LibDMSpace.csproj", "{EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,6 +116,10 @@ Global {F3312DE9-4335-4E85-A4CF-2616427A651E}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3312DE9-4335-4E85-A4CF-2616427A651E}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3312DE9-4335-4E85-A4CF-2616427A651E}.Release|Any CPU.Build.0 = Release|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {F4E241C3-0300-4B87-8707-BCBDEF1F0185} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} |