diff options
author | Rory& <root@rory.gay> | 2024-02-23 13:57:06 +0100 |
---|---|---|
committer | Rory& <root@rory.gay> | 2024-02-23 13:57:06 +0100 |
commit | d0d11db2209a8be65c27e15ca9d8a3b594f1a352 (patch) | |
tree | b42b7de4b09888a1439d0939707ba1331becf626 | |
parent | HS emulator (diff) | |
download | MatrixUtils-d0d11db2209a8be65c27e15ca9d8a3b594f1a352.tar.xz |
Add eons of work because I forgot to push
48 files changed, 1917 insertions, 364 deletions
diff --git a/.editorconfig b/.editorconfig index e98e832..1a3cf7e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -373,7 +373,8 @@ dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_property = false:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:error -file_header_template = # ReSharper properties +file_header_template = # ReSharper properties + resharper_alignment_tab_fill_style = use_spaces resharper_align_first_arg_by_paren = false @@ -608,8 +609,8 @@ resharper_line_break_before_requires_clause = do_not_change resharper_linkage_specification_braces = end_of_line resharper_linkage_specification_indentation = none resharper_local_function_body = expression_body -resharper_macro_block_begin = -resharper_macro_block_end = +resharper_macro_block_begin = +resharper_macro_block_end = resharper_max_array_initializer_elements_on_line = 10000 resharper_max_attribute_length_for_same_line = 38 resharper_max_enum_members_on_line = 3 @@ -624,7 +625,7 @@ resharper_new_line_before_catch = true resharper_new_line_before_else = true resharper_new_line_before_enumerators = true resharper_normalize_tag_names = false -resharper_no_indent_inside_elements = +resharper_no_indent_inside_elements = resharper_no_indent_inside_if_element_longer_than = 2000000 resharper_null_checking_pattern_style = not_null_pattern resharper_object_creation_when_type_evident = target_typed @@ -677,7 +678,7 @@ resharper_requires_expression_braces = next_line resharper_resx_allow_far_alignment = false resharper_resx_attribute_indent = single_indent resharper_resx_insert_final_newline = false -resharper_resx_linebreak_before_elements = +resharper_resx_linebreak_before_elements = resharper_resx_max_blank_lines_between_tags = 0 resharper_resx_max_line_length = 2147483647 resharper_resx_pi_attribute_style = do_not_touch @@ -904,7 +905,7 @@ resharper_xmldoc_wrap_text = true resharper_xml_allow_far_alignment = false resharper_xml_attribute_indent = align_by_first_attribute resharper_xml_insert_final_newline = false -resharper_xml_linebreak_before_elements = +resharper_xml_linebreak_before_elements = resharper_xml_max_blank_lines_between_tags = 2 resharper_xml_max_line_length = 180 resharper_xml_pi_attribute_style = do_not_touch diff --git a/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml b/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml index f74ab1c..0aa65bb 100644 --- a/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml +++ b/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml @@ -12,6 +12,7 @@ <entry key="MatrixRoomUtils.Desktop/MainWindow.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" /> <entry key="MatrixRoomUtils.Desktop/NavigationStack.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" /> <entry key="MatrixRoomUtils.Desktop/RoomListEntry.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" /> + <entry key="MatrixUtils.Desktop/Components/RoomListEntry.axaml" value="MatrixUtils.Desktop/MatrixUtils.Desktop.csproj" /> </map> </option> </component> diff --git a/.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..55540ea --- /dev/null +++ b/.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,22 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true"> + <option name="myValues"> + <value> + <list size="7"> + <item index="0" class="java.lang.String" itemvalue="nobr" /> + <item index="1" class="java.lang.String" itemvalue="noembed" /> + <item index="2" class="java.lang.String" itemvalue="comment" /> + <item index="3" class="java.lang.String" itemvalue="noscript" /> + <item index="4" class="java.lang.String" itemvalue="embed" /> + <item index="5" class="java.lang.String" itemvalue="script" /> + <item index="6" class="java.lang.String" itemvalue="width" /> + </list> + </value> + </option> + <option name="myCustomValuesEnabled" value="true" /> + </inspection_tool> + <inspection_tool class="JsonStandardCompliance" enabled="false" level="ERROR" enabled_by_default="false" /> + </profile> +</component> \ No newline at end of file diff --git a/LibMatrix b/LibMatrix -Subproject 3dfb7b81b0fe19d37a7bf1183e248ca10c56277 +Subproject c7b7dbe3d929d787fe0c76015082a117c422227 diff --git a/MatrixRoomUtils.sln b/MatrixRoomUtils.sln index 9cccf09..924697b 100644 --- a/MatrixRoomUtils.sln +++ b/MatrixRoomUtils.sln @@ -55,6 +55,9 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "LibMatrix\Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{5CE239F8-C124-4A96-A0F8-B56B9AE27434}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "LibMatrix\Tests\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{6D93DA72-69D8-43BD-BC19-7FFF8A313971}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "LibMatrix\ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.DmSpaced", "MatrixUtils.DmSpaced\MatrixUtils.DmSpaced.csproj", "{B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -150,6 +153,14 @@ Global {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Release|Any CPU.Build.0 = Release|Any CPU + {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Release|Any CPU.Build.0 = Release|Any CPU + {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {F4E241C3-0300-4B87-8707-BCBDEF1F0185} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} @@ -172,5 +183,6 @@ Global {CC836863-0EE8-44BD-BF39-45076F57C416} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D} {5CE239F8-C124-4A96-A0F8-B56B9AE27434} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D} {6D93DA72-69D8-43BD-BC19-7FFF8A313971} = {7D2C9959-8309-4110-A67F-DEE64E97C1D8} + {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14} EndGlobalSection EndGlobal diff --git a/MatrixUtils.Abstractions/RoomInfo.cs b/MatrixUtils.Abstractions/RoomInfo.cs index 877246b..53acbee 100644 --- a/MatrixUtils.Abstractions/RoomInfo.cs +++ b/MatrixUtils.Abstractions/RoomInfo.cs @@ -11,16 +11,17 @@ using LibMatrix.RoomTypes; namespace MatrixUtils.Abstractions; public class RoomInfo : NotifyPropertyChanged { - public required GenericRoom Room { get; set; } - public ObservableCollection<StateEventResponse?> StateEvents { get; } = new(); + public readonly GenericRoom Room; + public ObservableCollection<StateEventResponse?> StateEvents { get; private set; } = new(); private static ConcurrentBag<AuthenticatedHomeserverGeneric> homeserversWithoutEventFormatSupport = new(); - + private static SvgIdenticonGenerator identiconGenerator = new(); + public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") { if (homeserversWithoutEventFormatSupport.Contains(Room.Homeserver)) return await GetStateEventForged(type, stateKey); var @event = StateEvents.FirstOrDefault(x => x?.Type == type && x.StateKey == stateKey); if (@event is not null) return @event; - + try { @event = await Room.GetStateEventOrNullAsync(type, stateKey); StateEvents.Add(@event); @@ -30,6 +31,7 @@ public class RoomInfo : NotifyPropertyChanged { homeserversWithoutEventFormatSupport.Add(Room.Homeserver); return await GetStateEventForged(type, stateKey); } + Console.Error.WriteLine(e); await Task.Delay(1000); return await GetStateEvent(type, stateKey); @@ -79,7 +81,7 @@ public class RoomInfo : NotifyPropertyChanged { } public string? RoomIcon { - get => _roomIcon ?? "https://api.dicebear.com/6.x/identicon/svg?seed=" + Room.RoomId; + get => _roomIcon ?? _fallbackIcon; set => SetField(ref _roomIcon, value); } @@ -93,18 +95,17 @@ public class RoomInfo : NotifyPropertyChanged { set => SetField(ref _creationEventContent, value); } + private string? _roomCreator; + public string? RoomCreator { get => _roomCreator; set => SetField(ref _roomCreator, value); } - // public string? GetRoomIcon() => (StateEvents.FirstOrDefault(x => x?.Type == RoomAvatarEventContent.EventId)?.TypedContent as RoomAvatarEventContent)?.Url ?? - // "mxc://rory.gay/dgP0YPjJEWaBwzhnbyLLwGGv"; - private string? _roomIcon; + private readonly string _fallbackIcon; private string? _roomName; private RoomCreateEventContent? _creationEventContent; - private string? _roomCreator; private string? _overrideRoomType; private string? _defaultRoomName; private RoomMemberEventContent? _ownMembership; @@ -130,11 +131,25 @@ public class RoomInfo : NotifyPropertyChanged { set => SetField(ref _ownMembership, value); } - public RoomInfo() { + public RoomInfo(GenericRoom room) { + Room = room; + _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId); + registerEventListener(); + } + + public RoomInfo(GenericRoom room, List<StateEventResponse>? stateEvents) { + Room = room; + _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId); + if (stateEvents is { Count: > 0 }) StateEvents = new(stateEvents!); + registerEventListener(); + } + + private void registerEventListener() { StateEvents.CollectionChanged += (_, args) => { if (args.NewItems is { Count: > 0 }) - foreach (StateEventResponse? newState in args.NewItems) { // TODO: switch statement benchmark? - if(newState is null) continue; + foreach (StateEventResponse? newState in args.NewItems) { + // TODO: switch statement benchmark? + if (newState is null) continue; if (newState.Type == RoomNameEventContent.EventId && newState.TypedContent is RoomNameEventContent roomNameContent) RoomName = roomNameContent.Name; else if (newState is { Type: RoomAvatarEventContent.EventId, TypedContent: RoomAvatarEventContent roomAvatarContent }) @@ -146,4 +161,11 @@ public class RoomInfo : NotifyPropertyChanged { } }; } -} + + public async Task FetchAllStateAsync() { + var stateEvents = Room.GetFullStateAsync(); + await foreach (var stateEvent in stateEvents) { + StateEvents.Add(stateEvent); + } + } +} \ No newline at end of file diff --git a/MatrixUtils.Desktop/MainWindow.axaml.cs b/MatrixUtils.Desktop/MainWindow.axaml.cs index 562ab1a..9c783e4 100644 --- a/MatrixUtils.Desktop/MainWindow.axaml.cs +++ b/MatrixUtils.Desktop/MainWindow.axaml.cs @@ -42,9 +42,7 @@ public partial class MainWindow : Window { // roomList.Children.Add(new RoomListEntry(_scopeFactory, new RoomInfo(room))); windowContent.Push("home", new RoomListEntry() { - Room = new RoomInfo() { - Room = room - } + Room = new RoomInfo(room) }); base.OnLoaded(e); } diff --git a/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj new file mode 100644 index 0000000..4b0f599 --- /dev/null +++ b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj @@ -0,0 +1,31 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <LangVersion>preview</LangVersion> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <PublishAot>false</PublishAot> + <InvariantGlobalization>true</InvariantGlobalization> + <!-- <PublishTrimmed>true</PublishTrimmed>--> + <!-- <PublishReadyToRun>true</PublishReadyToRun>--> + <!-- <PublishSingleFile>true</PublishSingleFile>--> + <!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>--> + <!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>--> + <!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>--> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> + </ItemGroup> + <ItemGroup> + <Content Include="appsettings*.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + </ItemGroup> +</Project> diff --git a/MatrixUtils.DmSpaced/ModerationBot.cs b/MatrixUtils.DmSpaced/ModerationBot.cs new file mode 100644 index 0000000..6e534fc --- /dev/null +++ b/MatrixUtils.DmSpaced/ModerationBot.cs @@ -0,0 +1,298 @@ +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.Policy; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.RoomTypes; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace ModerationBot; + +public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<ModerationBot> logger, ModerationBotConfiguration configuration, PolicyEngine engine) : IHostedService { + private Task _listenerTask; + + // private GenericRoom _policyRoom; + private GenericRoom? _logRoom; + private GenericRoom? _controlRoom; + + /// <summary>Triggered when the application host is ready to start the service.</summary> + /// <param name="cancellationToken">Indicates that the start process has been aborted.</param> + public async Task StartAsync(CancellationToken cancellationToken) { + _listenerTask = Run(cancellationToken); + logger.LogInformation("Bot started!"); + } + + private async Task Run(CancellationToken cancellationToken) { + if (Directory.Exists("bot_data/cache")) + Directory.GetFiles("bot_data/cache").ToList().ForEach(File.Delete); + + BotData botData; + + try { + logger.LogInformation("Fetching bot account data..."); + botData = await hs.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data"); + logger.LogInformation("Got bot account data..."); + } + catch (Exception e) { + logger.LogInformation("Could not fetch bot account data... {}", e.Message); + if (e is not MatrixException { ErrorCode: "M_NOT_FOUND" }) { + logger.LogError("{}", e.ToString()); + throw; + } + + botData = null; + } + + botData = await FirstRunTasks.ConstructBotData(hs, configuration, botData); + + // _policyRoom = hs.GetRoom(botData.PolicyRoom ?? botData.ControlRoom); + _logRoom = hs.GetRoom(botData.LogRoom ?? botData.ControlRoom); + _controlRoom = hs.GetRoom(botData.ControlRoom); + foreach (var configurationAdmin in configuration.Admins) { + var pls = await _controlRoom.GetPowerLevelsAsync(); + if (pls is null) { + await _logRoom?.SendMessageEventAsync(MessageFormatter.FormatWarning($"Control room has no m.room.power_levels?")); + continue; + } + pls.SetUserPowerLevel(configurationAdmin, pls.GetUserPowerLevel(hs.UserId)); + await _controlRoom.SendStateEventAsync(RoomPowerLevelEventContent.EventId, pls); + } + var syncHelper = new SyncHelper(hs); + + List<string> admins = new(); + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(async () => { + while (!cancellationToken.IsCancellationRequested) { + var controlRoomMembers = _controlRoom.GetMembersEnumerableAsync(); + var pls = await _controlRoom.GetPowerLevelsAsync(); + await foreach (var member in controlRoomMembers) { + if ((member.TypedContent as RoomMemberEventContent)? + .Membership == "join" && pls.UserHasTimelinePermission(member.Sender, RoomMessageEventContent.EventId)) admins.Add(member.StateKey); + } + + await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken); + } + }, cancellationToken); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + syncHelper.InviteReceivedHandlers.Add(async Task (args) => { + var inviteEvent = + args.Value.InviteState.Events.FirstOrDefault(x => + x.Type == "m.room.member" && x.StateKey == hs.UserId); + logger.LogInformation("Got invite to {RoomId} by {Sender} with reason: {Reason}", args.Key, inviteEvent!.Sender, + (inviteEvent.TypedContent as RoomMemberEventContent)!.Reason); + await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Bot invited to {MessageFormatter.HtmlFormatMention(args.Key)} by {MessageFormatter.HtmlFormatMention(inviteEvent.Sender)}")); + if (admins.Contains(inviteEvent.Sender)) { + try { + await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joining {MessageFormatter.HtmlFormatMention(args.Key)}...")); + var senderProfile = await hs.GetProfileAsync(inviteEvent.Sender); + await hs.GetRoom(args.Key).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!"); + } + catch (Exception e) { + logger.LogError("{}", e.ToString()); + await _logRoom.SendMessageEventAsync(MessageFormatter.FormatException("Could not join room", e)); + await hs.GetRoom(args.Key).LeaveAsync(reason: "I was unable to join the room: " + e); + } + } + }); + + syncHelper.TimelineEventHandlers.Add(async @event => { + var room = hs.GetRoom(@event.RoomId); + try { + logger.LogInformation( + "Got timeline event in {}: {}", @event.RoomId, @event.ToJson(indent: true, ignoreNull: true)); + + if (@event != null && ( + @event.MappedType.IsAssignableTo(typeof(BasePolicy)) + || @event.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent)) + )) { + await LogPolicyChange(@event); + await engine.ReloadActivePolicyListById(@event.RoomId); + } + + var rules = await engine.GetMatchingPolicies(@event); + foreach (var matchedRule in rules) { + await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccessJson( + $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.OriginalEvent.EventId, roomId: matchedRule.PolicyList.Room.RoomId, displayName: "rule")}", @matchedRule.OriginalEvent.RawContent)); + } + + if (configuration.DemoMode) { + // foreach (var matchedRule in rules) { + // await room.SendMessageEventAsync(MessageFormatter.FormatSuccessJson( + // $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.EventId, roomId: matchedRule.RoomId, displayName: "rule")}", @matchedRule.RawContent)); + // } + return; + } + // + // if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventContent message }) { + // if (message is { MessageType: "m.image" }) { + // //check media + // // var matchedPolicy = await CheckMedia(@event); + // var matchedPolicy = rules.FirstOrDefault(); + // if (matchedPolicy is null) return; + // var matchedpolicyData = matchedPolicy.TypedContent as MediaPolicyEventContent; + // await _logRoom.SendMessageEventAsync( + // new RoomMessageEventContent( + // body: + // $"User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: {matchedPolicy.RawContent!.ToJson(ignoreNull: true)}", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = + // $"<font color=\"#FFFF00\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: <pre>{matchedPolicy.RawContent!.ToJson(ignoreNull: true)}</pre></font>" + // }); + // switch (matchedpolicyData.Recommendation) { + // case "warn_admins": { + // await _controlRoom.SendMessageEventAsync( + // new RoomMessageEventContent( + // body: $"{string.Join(' ', admins)}\nUser {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image {message.Url}", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = $"{string.Join(' ', admins.Select(u => MessageFormatter.HtmlFormatMention(u)))}\n" + + // $"<font color=\"#FF0000\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image <a href=\"{message.Url}\">{message.Url}</a></font>" + // }); + // break; + // } + // case "warn": { + // await room.SendMessageEventAsync( + // new RoomMessageEventContent( + // body: $"Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = + // $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}</a></font>" + // }); + // break; + // } + // case "redact": { + // await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason ?? "No reason specified"); + // break; + // } + // case "spoiler": { + // // <blockquote> + // // <a href=\"https://matrix.to/#/@emma:rory.gay\">@emma:rory.gay</a><br> + // // <a href=\"https://codeberg.org/crimsonfork/CN\"></a> + // // <font color=\"#dc143c\" data-mx-color=\"#dc143c\"> + // // <b>CN</b> + // // </font>: + // // <a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\">test</a><br> + // // <span data-mx-spoiler=\"\"><a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\"> + // // <img src=\"mxc://rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\" height=\"69\"></a> + // // </span> + // // </blockquote> + // await room.SendMessageEventAsync( + // new RoomMessageEventContent( + // body: + // $"Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = + // $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:</a></font>" + // }); + // var imageUrl = message.Url; + // await room.SendMessageEventAsync( + // new RoomMessageEventContent(body: $"CN: {imageUrl}", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = $""" + // <blockquote> + // <font color=\"#dc143c\" data-mx-color=\"#dc143c\"> + // <b>CN</b> + // </font>: + // <a href=\"{imageUrl}\">{matchedpolicyData.Reason}</a><br> + // <span data-mx-spoiler=\"\"> + // <a href=\"{imageUrl}\"> + // <img src=\"{imageUrl}\" height=\"69\"> + // </a> + // </span> + // </blockquote> + // """ + // }); + // await room.RedactEventAsync(@event.EventId, "Automatically spoilered: " + matchedpolicyData.Reason); + // break; + // } + // case "mute": { + // await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + // //change powerlevel to -1 + // var currentPls = await room.GetPowerLevelsAsync(); + // if (currentPls is null) { + // logger.LogWarning("Unable to get power levels for {room}", room.RoomId); + // await _logRoom.SendMessageEventAsync( + // MessageFormatter.FormatError($"Unable to get power levels for {MessageFormatter.HtmlFormatMention(room.RoomId)}")); + // return; + // } + // + // currentPls.Users ??= new(); + // currentPls.Users[@event.Sender] = -1; + // await room.SendStateEventAsync("m.room.power_levels", currentPls); + // break; + // } + // case "kick": { + // await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + // await room.KickAsync(@event.Sender, matchedpolicyData.Reason); + // break; + // } + // case "ban": { + // await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + // await room.BanAsync(@event.Sender, matchedpolicyData.Reason); + // break; + // } + // default: { + // throw new ArgumentOutOfRangeException("recommendation", + // $"Unknown response type {matchedpolicyData.Recommendation}!"); + // } + // } + // } + // } + } + catch (Exception e) { + logger.LogError("{}", e.ToString()); + await _controlRoom.SendMessageEventAsync( + MessageFormatter.FormatException($"Unable to process event in {MessageFormatter.HtmlFormatMention(room.RoomId)}", e)); + await _logRoom.SendMessageEventAsync( + MessageFormatter.FormatException($"Unable to process event in {MessageFormatter.HtmlFormatMention(room.RoomId)}", e)); + await using var stream = new MemoryStream(e.ToString().AsBytes().ToArray()); + await _controlRoom.SendFileAsync("error.log.cs", stream); + await _logRoom.SendFileAsync("error.log.cs", stream); + } + }); + await engine.ReloadActivePolicyLists(); + await syncHelper.RunSyncLoopAsync(); + } + + private async Task LogPolicyChange(StateEventResponse changeEvent) { + var room = hs.GetRoom(changeEvent.RoomId!); + var message = MessageFormatter.FormatWarning($"Policy change detected in {MessageFormatter.HtmlFormatMessageLink(changeEvent.RoomId, changeEvent.EventId, [hs.ServerName], await room.GetNameOrFallbackAsync())}!"); + message = message.ConcatLine(new RoomMessageEventContent(body: $"Policy type: {changeEvent.Type} -> {changeEvent.MappedType.Name}") { + FormattedBody = $"Policy type: {changeEvent.Type} -> {changeEvent.MappedType.Name}" + }); + var isUpdated = changeEvent.Unsigned.PrevContent is { Count: > 0 }; + var isRemoved = changeEvent.RawContent is not { Count: > 0 }; + // if (isUpdated) { + // message = message.ConcatLine(MessageFormatter.FormatSuccess("Rule updated!")); + // message = message.ConcatLine(MessageFormatter.FormatSuccessJson("Old rule:", changeEvent.Unsigned.PrevContent!)); + // } + // else if (isRemoved) { + // message = message.ConcatLine(MessageFormatter.FormatWarningJson("Rule removed!", changeEvent.Unsigned.PrevContent!)); + // } + // else { + // message = message.ConcatLine(MessageFormatter.FormatSuccess("New rule added!")); + // } + message = message.ConcatLine(MessageFormatter.FormatSuccessJson($"{(isUpdated ? "Updated" : isRemoved ? "Removed" : "New")} rule: {changeEvent.StateKey}", changeEvent.RawContent!)); + if (isRemoved || isUpdated) { + message = message.ConcatLine(MessageFormatter.FormatSuccessJson("Old content: ", changeEvent.Unsigned.PrevContent!)); + } + + await _logRoom.SendMessageEventAsync(message); + } + + /// <summary>Triggered when the application host is performing a graceful shutdown.</summary> + /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param> + public async Task StopAsync(CancellationToken cancellationToken) { + logger.LogInformation("Shutting down bot!"); + } + +} diff --git a/MatrixUtils.DmSpaced/ModerationBotConfiguration.cs b/MatrixUtils.DmSpaced/ModerationBotConfiguration.cs new file mode 100644 index 0000000..47e6423 --- /dev/null +++ b/MatrixUtils.DmSpaced/ModerationBotConfiguration.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Configuration; + +namespace ModerationBot; + +public class ModerationBotConfiguration { + public ModerationBotConfiguration(IConfiguration config) => config.GetRequiredSection("ModerationBot").Bind(this); + + public string Homeserver { get; set; } + public string AccessToken { get; set; } +} diff --git a/MatrixUtils.DmSpaced/Program.cs b/MatrixUtils.DmSpaced/Program.cs new file mode 100644 index 0000000..ae352b7 --- /dev/null +++ b/MatrixUtils.DmSpaced/Program.cs @@ -0,0 +1,40 @@ +// See https://aka.ms/new-console-template for more information + +using LibMatrix.Services; +using LibMatrix.Utilities.Bot; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using ModerationBot; +using ModerationBot.Services; + +Console.WriteLine("Hello, World!"); + +var builder = Host.CreateDefaultBuilder(args); + +builder.ConfigureHostOptions(host => { + host.ServicesStartConcurrently = true; + host.ServicesStopConcurrently = true; + host.ShutdownTimeout = TimeSpan.FromSeconds(5); +}); + +if (Environment.GetEnvironmentVariable("MODERATIONBOT_APPSETTINGS_PATH") is string path) + builder.ConfigureAppConfiguration(x => x.AddJsonFile(path)); + +var host = builder.ConfigureServices((_, services) => { + services.AddScoped<TieredStorageService>(x => + new TieredStorageService( + cacheStorageProvider: new FileStorageProvider("bot_data/cache/"), + dataStorageProvider: new FileStorageProvider("bot_data/data/") + ) + ); + services.AddSingleton<ModerationBotConfiguration>(); + + services.AddRoryLibMatrixServices(); + + services.AddSingleton<ModerationBotRoomProvider>(); + + services.AddHostedService<ModerationBot.ModerationBot>(); +}).UseConsoleLifetime().Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/MatrixUtils.DmSpaced/appsettings.Development.json b/MatrixUtils.DmSpaced/appsettings.Development.json new file mode 100644 index 0000000..224d0da --- /dev/null +++ b/MatrixUtils.DmSpaced/appsettings.Development.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "LibMatrixBot": { + // The homeserver to connect to + "Homeserver": "rory.gay", + // The access token to use + "AccessToken": "syt_xxxxxxxxxxxxxxxxx", + // The command prefix + "Prefix": "?" + }, + "ModerationBot": { + // List of people who should be invited to the control room + "Admins": [ + "@emma:conduit.rory.gay", + "@emma:rory.gay" + ] + } +} diff --git a/MatrixUtils.DmSpaced/appsettings.json b/MatrixUtils.DmSpaced/appsettings.json new file mode 100644 index 0000000..6ba02f3 --- /dev/null +++ b/MatrixUtils.DmSpaced/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs index 3fb0ab6..e2c8192 100644 --- a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs +++ b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs @@ -1,6 +1,10 @@ +using System.Net; using ArcaneLibs.Extensions; using LibMatrix; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Homeservers; +using LibMatrix.Responses; using LibMatrix.RoomTypes; using MatrixUtils.LibDMSpace.StateEvents; @@ -9,75 +13,179 @@ namespace MatrixUtils.LibDMSpace; public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : SpaceRoom(homeserver, roomId) { private readonly GenericRoom _room; - public async Task<DMSpaceInfo?> GetDmSpaceInfo() { + // ReSharper disable once InconsistentNaming + public async Task<DMSpaceInfo?> GetDMSpaceInfo() { return await GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId); } - public async IAsyncEnumerable<GenericRoom> GetChildrenAsync(bool includeRemoved = false) { - var rooms = new List<GenericRoom>(); + public async Task<Dictionary<string, List<string>>?> ExportNativeDMs() { 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.GetMembersEnumerableAsync(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 mdirect = new Dictionary<string, List<string>>(); + await foreach (var stateEvent in state) { } - var resp = await SendStateEventAsync("m.space.child", room.RoomId, new { - via = memberCountByHs - .OrderByDescending(x => x.Value) - .Select(x => x.Key) - .Take(10) - }); - return resp; + return mdirect; } public async Task ImportNativeDMs() { - var dmSpaceInfo = await GetDmSpaceInfo(); + 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 + public async Task<List<StateEventResponse>> GetAllActiveLayersAsync() { + var state = await GetFullStateAsListAsync(); + return state.Where(x => x.Type == DMSpaceChildLayer.EventId && x.RawContent.ContainsKey("space_id")).ToList(); + } + +#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 - } + AttributedUser = userId }; - // Add all DM room members - var members = homeserver.GetRoom(roomid).GetMembersEnumerableAsync(); - await foreach (var member in members) - if (member.StateKey != userId) - dri.RemoteUsers.Add(member.StateKey); - // Remove members of DM space - members = GetMembersEnumerableAsync(); - await foreach (var member in members) - if (dri.RemoteUsers.Contains(member.StateKey)) - dri.RemoteUsers.Remove(member.StateKey); await SendStateEventAsync(DMRoomInfo.EventId, roomid, dri); + await AddChildAsync(Homeserver.GetRoom(roomid)); } } } private async Task ImportNativeDMsIntoLayers() { var mdirect = await homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + var layerTasks = mdirect.Select(async entry => { + var (userId, dmRooms) = entry; + DMSpaceChildLayer? layer = await GetStateOrNullAsync<DMSpaceChildLayer>(DMSpaceChildLayer.EventId, userId.UrlEncode()) ?? await CreateLayer(userId); + return (entry, layer); + }).ToAsyncEnumerable(); + + await foreach (var ((userId, dmRooms), layer) in layerTasks) { + var space = Homeserver.GetRoom(layer.SpaceId).AsSpace; + foreach (var roomid in dmRooms) { + var dri = new DMRoomInfo() { + AttributedUser = userId + }; + await space.SendStateEventAsync(DMRoomInfo.EventId, roomid, dri); + await space.AddChildAsync(Homeserver.GetRoom(roomid)); + } + + await UpdateLayer(layer, userId); + } + + // ensure all spaces are linked + Console.WriteLine("Ensuring all child layers are inside space..."); + var layerAssuranceTasks = (await GetFullStateAsListAsync()) + .Where(x => x.Type == DMSpaceChildLayer.EventId && (x.RawContent?.ContainsKey("space_id") ?? false)) + .Select(async layer => { + var content = layer.TypedContent as DMSpaceChildLayer; + var space = homeserver.GetRoom(content!.SpaceId); + try { + var state = await space.GetFullStateAsListAsync(); + if (!state.Any(e => e.Type == DMRoomInfo.EventId)) throw new Exception(); + } + catch { + await homeserver.JoinRoomAsync(content!.SpaceId); + } + + return AddChildAsync(space); + }); + await Task.WhenAll(layerAssuranceTasks); + Console.WriteLine("All child layers should be inside of space, if not, something went horribly wrong!"); + } + + private async Task<DMSpaceChildLayer> CreateLayer(string userId) { + var childCreateRequest = CreateRoomRequest.CreatePrivate(homeserver, userId); + childCreateRequest.CreationContent["type"] = "m.space"; + + var layer = new DMSpaceChildLayer() { + SpaceId = (await homeserver.CreateRoom(childCreateRequest)).RoomId + }; + + await SendStateEventAsync(DMSpaceChildLayer.EventId, userId[1..], layer); + await AddChildAsync(Homeserver.GetRoom(layer.SpaceId)); + return layer; + } + + private async Task UpdateLayerProfilesAsync() { + var layers = await GetAllActiveLayersAsync(); + var getProfileTasks = layers.Select(async x => { + UserProfileResponse? profile = null; + try { + return (x, profile); + } + catch { + return (x, null); + } + + }).ToAsyncEnumerable(); + await foreach (var (layer, profile) in getProfileTasks) { + if (profile is null) continue; + var layerContent = layer.TypedContent as DMSpaceChildLayer; + var space = Homeserver.GetRoom(layerContent!.SpaceId).AsSpace; + + try { + await space.SendStateEventAsync(RoomAvatarEventContent.EventId, "", new RoomAvatarEventContent() { + Url = layerContent.OverrideAvatar ?? profile?.AvatarUrl + }); + await space.SendStateEventAsync(RoomNameEventContent.EventId, "", new RoomNameEventContent() { + Name = layerContent.OverrideName ?? profile?.DisplayName ?? "@" + layer.StateKey.UrlDecode() + }); + } + catch (MatrixException e) { + Console.WriteLine("Failed to update space: {0}", e); + } + } + } + + private async Task UpdateLayer(DMSpaceChildLayer layer, string mxid) { + UserProfileResponse? profile = null; + var space = Homeserver.GetRoom(layer.SpaceId).AsSpace; + + if (string.IsNullOrWhiteSpace(layer.OverrideAvatar) || string.IsNullOrWhiteSpace(layer.OverrideName)) { + try { + profile = await homeserver.GetProfileAsync(mxid); + } + catch (MatrixException e) { + // if (e.ErrorCode != "M_NOT_FOUND") throw; + Console.Error.WriteLine(e); + } + } + + try { + await space.SendStateEventAsync(RoomAvatarEventContent.EventId, "", new RoomAvatarEventContent() { + Url = layer.OverrideAvatar ?? profile?.AvatarUrl + }); + await space.SendStateEventAsync(RoomNameEventContent.EventId, "", new RoomNameEventContent() { + Name = layer.OverrideName ?? profile?.DisplayName ?? mxid + }); + } + catch (MatrixException e) { + Console.WriteLine("Failed to update space: {0}", e); + } + } + + public async Task DisbandDMSpace() { + var state = await GetFullStateAsListAsync(); + var leaveTasks = state.Select(async x => { + if (x.Type != DMSpaceChildLayer.EventId) return; + var content = x.TypedContent as DMSpaceChildLayer; + if (content?.SpaceId is null) return; + var space = homeserver.GetRoom(content.SpaceId); + try { + await space.LeaveAsync(); + } + catch { + // might not be in room, doesnt matter + } + }); + + await LeaveAsync(); + + await Task.WhenAll(leaveTasks); } - #endregion -} +#endregion +} \ No newline at end of file diff --git a/MatrixUtils.LibDMSpace/HomeserverExtensions.cs b/MatrixUtils.LibDMSpace/HomeserverExtensions.cs new file mode 100644 index 0000000..2bf5eaa --- /dev/null +++ b/MatrixUtils.LibDMSpace/HomeserverExtensions.cs @@ -0,0 +1,10 @@ +using LibMatrix.Homeservers; + +namespace MatrixUtils.LibDMSpace; + +public static class HomeserverExtensions { + public static async Task<DMSpaceRoom> GetDMSpaceRoomAsync(this AuthenticatedHomeserverGeneric homeserver) { + return null; //TODO: implement + } + +} \ No newline at end of file diff --git a/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs b/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs index 5aa62d7..bc595b5 100644 --- a/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs +++ b/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs @@ -5,10 +5,13 @@ using LibMatrix.Interfaces; namespace MatrixUtils.LibDMSpace.StateEvents; [MatrixEvent(EventName = EventId)] -public class DMRoomInfo : TimelineEventContent { +public class DMRoomInfo : EventContent { public const string EventId = "gay.rory.dm_room_info"; - [JsonPropertyName("remote_users")] - public List<string> RemoteUsers { get; set; } + // [JsonPropertyName("remote_users")] + // public List<string> RemoteUsers { get; set; } + [JsonPropertyName("attributed_user")] + public string AttributedUser { get; set; } + } diff --git a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs new file mode 100644 index 0000000..16c7b70 --- /dev/null +++ b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; +using LibMatrix.EventTypes; +using LibMatrix.Interfaces; + +namespace MatrixUtils.LibDMSpace.StateEvents; + +[MatrixEvent(EventName = EventId)] +public class DMSpaceChildLayer : EventContent { + public const string EventId = "gay.rory.dm_space_child_layer"; + + [JsonPropertyName("space_id")] + public string SpaceId { get; set; } + + [JsonPropertyName("override_name")] + public string? OverrideName { get; set; } + + [JsonPropertyName("override_avatar")] + public string? OverrideAvatar { get; set; } + +} diff --git a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs index 0df1913..f5daa74 100644 --- a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs +++ b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs @@ -5,7 +5,7 @@ using LibMatrix.Interfaces; namespace MatrixUtils.LibDMSpace.StateEvents; [MatrixEvent(EventName = EventId)] -public class DMSpaceInfo : TimelineEventContent { +public class DMSpaceInfo : EventContent { public const string EventId = "gay.rory.dm_space_info"; [JsonPropertyName("is_layered")] diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj index 515b235..dfb4713 100644 --- a/MatrixUtils.Web/MatrixUtils.Web.csproj +++ b/MatrixUtils.Web/MatrixUtils.Web.csproj @@ -11,6 +11,7 @@ <UseBlazorWebAssembly>true</UseBlazorWebAssembly> <BlazorEnableCompression>false</BlazorEnableCompression> <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest> + <BlazorCacheBootResources>false</BlazorCacheBootResources> <!-- <RunAOTCompilation>true</RunAOTCompilation>--> </PropertyGroup> diff --git a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor index 611d4c1..87416a2 100644 --- a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor +++ b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor @@ -12,9 +12,9 @@ else { <details> <summary>Room List</summary> - @foreach (var room in Rooms) { - <a style="color: unset; text-decoration: unset;" href="/RoomStateViewer/@room.Replace('.', '~')"> - <RoomListItem RoomInfo="@(new RoomInfo() { Room = hs.GetRoom(room) })" LoadData="true"></RoomListItem> + @foreach (var roomId in Rooms) { + <a style="color: unset; text-decoration: unset;" href="/RoomStateViewer/@roomId.Replace('.', '~')"> + <RoomListItem RoomInfo="@(new RoomInfo(hs.GetRoom(roomId)))" LoadData="true"></RoomListItem> </a> } </details> diff --git a/MatrixUtils.Web/Pages/HSEInit.razor b/MatrixUtils.Web/Pages/HSEInit.razor index 3020ff7..b2fc0db 100644 --- a/MatrixUtils.Web/Pages/HSEInit.razor +++ b/MatrixUtils.Web/Pages/HSEInit.razor @@ -6,7 +6,7 @@ @code { protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - var tasks = Enumerable.Range(0, 5000).Select(i => Login()).ToList(); + var tasks = Enumerable.Range(0, 50).Select(i => Login()).ToList(); await Task.WhenAll(tasks); Console.WriteLine("All logins complete!"); var userAuths = tasks.Select(t => t.Result).Where(t => t != null).ToList(); diff --git a/MatrixUtils.Web/Pages/Index.razor b/MatrixUtils.Web/Pages/Index.razor index 0c0c87a..f216488 100644 --- a/MatrixUtils.Web/Pages/Index.razor +++ b/MatrixUtils.Web/Pages/Index.razor @@ -3,6 +3,8 @@ @using LibMatrix.Responses @using LibMatrix @using ArcaneLibs.Extensions +@using ArcaneLibs +@using System.Diagnostics <PageTitle>Index</PageTitle> @@ -12,7 +14,10 @@ Small collection of tools to do not-so-everyday things. <br/><br/> <h5>@totalSessions signed in sessions - <a href="/Login">Add new account</a></h5> @if (scannedSessions != totalSessions) { - <progress max="@totalSessions" value="@scannedSessions"></progress> + <span> + <span>@scannedSessions/@totalSessions</span> + <progress max="@totalSessions" value="@scannedSessions"></progress> + </span> } <hr/> <form> @@ -103,6 +108,7 @@ Small collection of tools to do not-so-everyday things. private readonly List<UserAuth> _offlineSessions = []; private LoginResponse? _currentSession; int scannedSessions = 0, totalSessions = 1; + private SvgIdenticonGenerator _identiconGenerator = new(); protected override async Task OnInitializedAsync() { Console.WriteLine("Index.OnInitializedAsync"); @@ -124,6 +130,7 @@ Small collection of tools to do not-so-everyday things. List<string> offlineServers = []; var sema = new SemaphoreSlim(64, 64); + var updateSw = Stopwatch.StartNew(); var tasks = tokens.Select(async token => { await sema.WaitAsync(); scannedSessions++; @@ -141,7 +148,7 @@ Small collection of tools to do not-so-everyday things. var serverVersionTask = hs.FederationClient?.GetServerVersionAsync(); _sessions.Add(new() { UserInfo = new() { - AvatarUrl = "/blobfox_outage.gif", + AvatarUrl = string.IsNullOrWhiteSpace((await profileTask).AvatarUrl) ? _identiconGenerator.GenerateAsDataUri(hs.WhoAmI.UserId) : hs.ResolveMediaUri((await profileTask).AvatarUrl), RoomCount = (await joinedRoomsTask).Count, DisplayName = (await profileTask).DisplayName ?? hs.WhoAmI.UserId }, @@ -149,6 +156,10 @@ Small collection of tools to do not-so-everyday things. ServerVersion = await (serverVersionTask ?? Task.FromResult<ServerVersionResponse?>(null)!), Homeserver = hs }); + if (updateSw.ElapsedMilliseconds > 250) { + updateSw.Restart(); + StateHasChanged(); + } } catch (MatrixException e) { if (e is { ErrorCode: "M_UNKNOWN_TOKEN" }) _offlineSessions.Add(token); @@ -166,50 +177,9 @@ Small collection of tools to do not-so-everyday things. } sema.Release(); - - StateHasChanged(); }).ToList(); await Task.WhenAll(tasks); - - // var profileTasks = tokens.Select(async token => { - // UserInfo userInfo = new(); - // AuthenticatedHomeserverGeneric hs; - // Console.WriteLine($"Getting hs for {token.ToJson()}"); - // try { - // hs = await hsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy); - // } - // catch (MatrixException e) { - // if (e.ErrorCode != "M_UNKNOWN_TOKEN") throw; - // _offlineSessions.Add(token); - // return; - // NavigationManager.NavigateTo("/InvalidSession?ctx=" + token.AccessToken); - // } - // catch (Exception e) { - // logger.LogError(e, $"Failed to instantiate AuthenticatedHomeserver for {token.ToJson()}, homeserver may be offline?", token.UserId); - // _offlineSessions.Add(token); - // return; - // } - // - // Console.WriteLine($"Got hs for {token.ToJson()}"); - // - // var roomCountTask = hs.GetJoinedRooms(); - // var profile = await hs.GetProfileAsync(hs.WhoAmI.UserId); - // userInfo.DisplayName = profile.DisplayName ?? hs.WhoAmI.UserId; - // Console.WriteLine(profile.ToJson()); - // _sessions.Add(new() { - // UserInfo = new() { - // AvatarUrl = string.IsNullOrWhiteSpace(profile.AvatarUrl) ? "/blobfox_outage.gif" : hs.ResolveMediaUri(profile.AvatarUrl), - // RoomCount = (await roomCountTask).Count, - // DisplayName = profile.DisplayName ?? hs.WhoAmI.UserId - // }, - // UserAuth = token, - // ServerVersion = await (hs.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)), - // Homeserver = hs - // }); - // }).ToList(); - // Console.WriteLine($"Waiting for {profileTasks.Count} profile tasks"); - // await Task.WhenAll(profileTasks); - // Console.WriteLine("Done waiting for profile tasks"); + await base.OnInitializedAsync(); } diff --git a/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor index 3cb9e40..f9cbfa2 100644 --- a/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor +++ b/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor @@ -59,7 +59,7 @@ if (hs is null) return; data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>("org.matrix.mjolnir.protected_rooms"); StateHasChanged(); - foreach (var room in await hs.GetJoinedRooms()) { + var tasks = (await hs.GetJoinedRooms()).Select(async room => { var plTask = room.GetPowerLevelsAsync(); var roomNameTask = room.GetNameOrFallbackAsync(); var EditorRoomInfo = new EditorRoomInfo { @@ -71,7 +71,11 @@ Rooms.Add(EditorRoomInfo); StateHasChanged(); - } + return Task.CompletedTask; + }).ToList(); + await Task.WhenAll(tasks); + await Task.Delay(500); + StateHasChanged(); } private class DraupnirProtectedRoomsData { @@ -87,7 +91,7 @@ } private async Task Apply() { - Console.WriteLine(string.Join('\n', Rooms.Where(x=>x.IsProtected).Select(x=>x.Room.RoomId))); + Console.WriteLine(string.Join('\n', Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId))); data.Rooms = Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId).ToList(); await hs.SetAccountDataAsync("org.matrix.mjolnir.protected_rooms", data); } diff --git a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor index 775361a..9218c8c 100644 --- a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor +++ b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor @@ -82,9 +82,7 @@ else { if (state is null) continue; if (!matchingStates.ContainsKey(state.Membership)) matchingStates.Add(state.Membership, new()); - var roomInfo = new RoomInfo() { - Room = room - }; + var roomInfo = new RoomInfo(room); matchingStates[state.Membership].Add(roomInfo); roomInfo.StateEvents.Add(new() { Type = RoomNameEventContent.EventId, diff --git a/MatrixUtils.Web/Pages/Rooms/Index.razor b/MatrixUtils.Web/Pages/Rooms/Index.razor index 1813908..d7a3569 100644 --- a/MatrixUtils.Web/Pages/Rooms/Index.razor +++ b/MatrixUtils.Web/Pages/Rooms/Index.razor @@ -69,14 +69,14 @@ protected override async Task OnInitializedAsync() { Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); if (Homeserver is null) return; - var rooms = await Homeserver.GetJoinedRooms(); + // var rooms = await Homeserver.GetJoinedRooms(); // SemaphoreSlim _semaphore = new(160, 160); GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId); var filter = await Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetBasicRoomInfo); var filterData = await Homeserver.GetFilterAsync(filter); - Rooms = new ObservableCollection<RoomInfo>(rooms.Select(x => new RoomInfo() { Room = x })); + // Rooms = new ObservableCollection<RoomInfo>(rooms.Select(room => new RoomInfo(room))); // foreach (var stateType in filterData.Room?.State?.Types ?? []) { // var tasks = Rooms.Select(async room => { // try { @@ -126,7 +126,7 @@ Console.WriteLine($"Queue no longer empty after {renderTimeSw.Elapsed}!"); - int maxUpdates = 50; + int maxUpdates = 50000; isInitialSync = false; while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) { var (roomId, roomData) = queueEntry; @@ -139,9 +139,7 @@ } else { Console.WriteLine($"QueueWorker: encountered new room {roomId}!"); - room = new RoomInfo() { - Room = Homeserver.GetRoom(roomId) - }; + room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.State?.Events); Rooms.Add(room); } @@ -156,14 +154,14 @@ Console.WriteLine($"QueueWorker: could not merge state for {room.Room.RoomId} as new data contains no state events!"); } - await Task.Delay(100); + // await Task.Delay(100); } Console.WriteLine($"QueueWorker: {queue.Count} entries left in queue, {maxUpdates} maxUpdates left, RenderContents: {RenderContents}"); Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue..."; - RenderContents |= queue.Count == 0; - await Task.Delay(Rooms.Count); + // RenderContents |= queue.Count == 0; + // await Task.Delay(Rooms.Count); } catch (Exception e) { Console.WriteLine("QueueWorker exception: " + e); diff --git a/MatrixUtils.Web/Pages/Rooms/Index2.razor b/MatrixUtils.Web/Pages/Rooms/Index2.razor new file mode 100644 index 0000000..ae31126 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2.razor @@ -0,0 +1,85 @@ +@page "/Rooms2" +@using LibMatrix.Responses +@using System.Collections.ObjectModel +@using System.ComponentModel +@using MatrixUtils.Abstractions +@using MatrixUtils.Web.Pages.Rooms.Index2Components +@inject ILogger<Index> logger +<h3>Room list</h3> + +<RoomsIndex2SyncContainer Data="@Data"></RoomsIndex2SyncContainer> +@if (Data.Homeserver is null || Data.GlobalProfile is null) { + <p>Creating homeserver instance and fetching global profile...</p> + return; +} + +<div> + <LinkButton Color="@(SelectedTab == Tab.Main ? null : "#0b0e62")" OnClick="() => Task.FromResult(SelectedTab = Tab.Main)">Main</LinkButton> + <LinkButton Color="@(SelectedTab == Tab.DMs ? null : "#0b0e62")" OnClick="() => Task.FromResult(SelectedTab = Tab.DMs)">DMs</LinkButton> + <LinkButton Color="@(SelectedTab == Tab.ByRoomType ? null : "#0b0e62")" OnClick="() => Task.FromResult(SelectedTab = Tab.ByRoomType)">By room type</LinkButton> +</div> +<br/> +<CascadingValue Value="@Data"> + @switch (SelectedTab) { + case Tab.Main: + <h3>Main tab</h3> + <RoomsIndex2MainTab></RoomsIndex2MainTab> + break; + case Tab.DMs: + <h3>DMs tab</h3> + break; + case Tab.ByRoomType: + <h3>By room type tab</h3> + break; + default: + throw new InvalidEnumArgumentException(); + } +</CascadingValue> +<br/> + +@* <LinkButton href="/Rooms/Create">Create new room</LinkButton> *@ + + +@code { + + private Tab SelectedTab { + get => _selectedTab; + set { + _selectedTab = value; + StateHasChanged(); + } + } + + public RoomListViewData Data { get; set; } = new RoomListViewData(); + + protected override async Task OnInitializedAsync() { + Data.Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + if (Data.Homeserver is null) return; + var rooms = await Data.Homeserver.GetJoinedRooms(); + Data.GlobalProfile = await Data.Homeserver.GetProfileAsync(Data.Homeserver.WhoAmI.UserId); + + foreach (var room in rooms) { + Data.Rooms.Add(new RoomInfo(room)); + } + StateHasChanged(); + + await base.OnInitializedAsync(); + } + + private Tab _selectedTab = Tab.Main; + + private enum Tab { + Main, + DMs, + ByRoomType + } + + public class RoomListViewData { + public ObservableCollection<RoomInfo> Rooms { get; } = []; + + public UserProfileResponse? GlobalProfile { get; set; } + + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor new file mode 100644 index 0000000..4216824 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor @@ -0,0 +1,27 @@ +@using MatrixUtils.Abstractions +<div class="spaceListItem" onclick="@ToggleSpace"> + <MxcImage Circular="true" Height="32" Width="32" Homeserver="Space.Room.Homeserver" MxcUri="@Space.RoomIcon"></MxcImage> + <span class="spaceNameEllipsis">@Space.RoomName</span> +</div> + +@code { + + [Parameter] + public RoomInfo Space { get; set; } + + [Parameter] + public List<RoomInfo> OpenedSpaces { get; set; } + + protected override Task OnInitializedAsync() { + Space.PropertyChanged += (sender, args) => { StateHasChanged(); }; + return base.OnInitializedAsync(); + } + + public void ToggleSpace() { + if (OpenedSpaces.Contains(Space)) { + OpenedSpaces.Remove(Space); + } else { + OpenedSpaces.Add(Space); + } + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css new file mode 100644 index 0000000..c174567 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css @@ -0,0 +1,15 @@ +.spaceNameEllipsis { + padding-left: 8px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; + width: calc(100% - 38px); +} + +.spaceListItem { + display: block; + width: 100%; + height: 50px; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2DMsTab.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2DMsTab.razor new file mode 100644 index 0000000..f4cf849 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2DMsTab.razor @@ -0,0 +1,53 @@ +@using MatrixUtils.Abstractions +@using System.Security.Cryptography +@using ArcaneLibs.Extensions +<h3>RoomsIndex2MainTab</h3> + +<div> + <div class="row"> + <div class="col-3" style="background-color: #ffffff66;"> + <LinkButton>Uncategorised rooms</LinkButton> + @foreach (var space in Data.Rooms.Where(x => x.RoomType == "m.space")) { + <div style="@("width: 100%; height: 50px; background-color: #" + RandomNumberGenerator.GetBytes(3).Append((byte)0x11).ToArray().AsHexString().Replace(" ",""))"> + <p>@space.RoomName</p> + </div> + } + </div> + <div class="col-9" style="background-color: #ff00ff66;"> + <p>omae wa mou shindeiru</p> + </div> + </div> +</div> + +@code { + + [CascadingParameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + protected override async Task OnInitializedAsync() { + Data.Rooms.CollectionChanged += (sender, args) => { + DebouncedStateHasChanged(); + if (args.NewItems is { Count: > 0 }) + foreach (var newItem in args.NewItems) { + (newItem as RoomInfo).PropertyChanged += (sender, args) => { DebouncedStateHasChanged(); }; + } + }; + await base.OnInitializedAsync(); + } + + //debounce StateHasChanged, we dont want to reredner on every key stroke + + private CancellationTokenSource _debounceCts = new CancellationTokenSource(); + + private async Task DebouncedStateHasChanged() { + _debounceCts.Cancel(); + _debounceCts = new CancellationTokenSource(); + try { + await Task.Delay(100, _debounceCts.Token); + Console.WriteLine("DebouncedStateHasChanged - Calling StateHasChanged!"); + StateHasChanged(); + } + catch (TaskCanceledException) { } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor new file mode 100644 index 0000000..2b7c5ac --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor @@ -0,0 +1,198 @@ +@using MatrixUtils.Abstractions +@using System.Security.Cryptography +@using ArcaneLibs.Extensions +@using System.ComponentModel +@using System.Diagnostics +@using LibMatrix.EventTypes.Spec.State +@using MatrixUtils.Web.Pages.Rooms.Index2Components.MainTabComponents +@using Microsoft.AspNetCore.Components.Rendering +<h3>RoomsIndex2MainTab</h3> + +@* <div> *@ +@* <div class="row"> *@ +@* <div class="col-3" style="background-color: #ffffff66;"> *@ +@* <LinkButton>Uncategorised rooms</LinkButton> *@ +@* @foreach (var space in GetTopLevelSpaces()) { *@ +@* <a style="@("display:block; width: 100%; height: 50px; background-color: #" + RandomNumberGenerator.GetBytes(3).Append((byte)0x11).ToArray().AsHexString().Replace(" ", ""))"> *@ +@* <div style="vertical-align: middle;"> *@ +@* <div style="overflow:hidden; text-overflow: ellipsis; white-space: nowrap; ">@space.RoomName</div> *@ +@* </div> *@ +@* </a> *@ +@* } *@ +@* </div> *@ +@* <div class="col-9" style="background-color: #ff00ff66;"> *@ +@* <p>Placeholder for rooms list...</p> *@ +@* </div> *@ +@* </div> *@ +@* </div> *@ + +<div> + <div class="row"> + <div class="col-3" style="background-color: #ffffff22;"> + <LinkButton>Uncategorised rooms</LinkButton> + @foreach (var space in GetTopLevelSpaces()) { + @RecursingSpaceChildren(space) + } + </div> + <div class="col-9" style="background-color: #ff00ff66;"> + <p>Placeholder for rooms list...</p> + </div> + </div> +</div> + + +@code { + + [CascadingParameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + protected override async Task OnInitializedAsync() { + Data.Rooms.CollectionChanged += (sender, args) => { + DebouncedStateHasChanged(); + if (args.NewItems is { Count: > 0 }) + foreach (var newItem in args.NewItems) { + (newItem as RoomInfo).PropertyChanged += OnRoomListChanged; + (newItem as RoomInfo).StateEvents.CollectionChanged += (sender, args) => { DebouncedStateHasChanged(); }; + } + }; + foreach (var newItem in Data.Rooms) { + newItem.PropertyChanged += OnRoomListChanged; + newItem.StateEvents.CollectionChanged += (sender, args) => { DebouncedStateHasChanged(); }; + } + + await base.OnInitializedAsync(); + StateHasChanged(); + } + + private void OnRoomListChanged(object? sender, PropertyChangedEventArgs e) { + if (e.PropertyName == "RoomName" || e.PropertyName == "RoomType") + DebouncedStateHasChanged(); + } + + private CancellationTokenSource _debounceCts = new CancellationTokenSource(); + + private async Task DebouncedStateHasChanged() { + _debounceCts.Cancel(); + _debounceCts = new CancellationTokenSource(); + try { + Console.WriteLine("DebouncedStateHasChanged - Waiting 50ms..."); + await Task.Delay(50, _debounceCts.Token); + Console.WriteLine("DebouncedStateHasChanged - Calling StateHasChanged!"); + StateHasChanged(); + } + catch (TaskCanceledException) { } + } + + private List<RoomInfo> GetTopLevelSpaces() { + var spaces = Data.Rooms.Where(x => x.RoomType == "m.space").OrderBy(x => x.RoomName).ToList(); + var allSpaceChildEvents = spaces.SelectMany(x => x.StateEvents.Where(y => + y.Type == SpaceChildEventContent.EventId && + y.RawContent!.Count > 0 + )).ToList(); + + Console.WriteLine($"Child count: {allSpaceChildEvents.Count}"); + + spaces.RemoveAll(x => allSpaceChildEvents.Any(y => y.StateKey == x.Room.RoomId)); + + if (allSpaceChildEvents.Count == 0) { + Console.WriteLine("No space children found, returning nothing..."); + return []; + } + + return spaces.ToList(); + } + + private List<RoomInfo> GetSpaceChildren(RoomInfo space) { + var childEvents = space.StateEvents.Where(x => + x.Type == SpaceChildEventContent.EventId && + x.RawContent!.Count > 0 + ).ToList(); + var children = childEvents.Select(x => Data.Rooms.FirstOrDefault(y => y.Room.RoomId == x.StateKey)).Where(x => x is not null).ToList(); + return children; + } + + private List<RoomInfo> GetSpaceChildSpaces(RoomInfo space) { + var children = GetSpaceChildren(space); + var childSpaces = children.Where(x => x.RoomType == "m.space").ToList(); + return childSpaces; + } + + private RoomInfo? SelectedSpace { get; set; } + private List<RoomInfo> OpenedSpaces { get; set; } = new List<RoomInfo>(); + + private RenderFragment RecursingSpaceChildren(RoomInfo space, List<RoomInfo>? parents = null, int depth = 0) { + parents ??= []; + var totalSw = Stopwatch.StartNew(); + var children = GetSpaceChildSpaces(space); + + var randomColor = RandomNumberGenerator.GetBytes(3).Append((byte)0x33).ToArray().AsHexString().Replace(" ", ""); + var isExpanded = OpenedSpaces.Contains(space); + + // Console.WriteLine($"RecursingSpaceChildren::FetchData - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {totalSw.Elapsed}"); + + // var renderSw = Stopwatch.StartNew(); + var rf = new RenderFragment(builder => { + builder.OpenElement(0, "div"); + //space list entry render fragment + // builder.AddContent(1, SpaceListEntry(space)); + builder.OpenComponent<MainTabSpaceItem>(1); + builder.AddAttribute(2, "Space", space); + builder.AddAttribute(2, "OpenedSpaces", OpenedSpaces); + builder.CloseComponent(); + builder.CloseElement(); + //space children render fragment + if (isExpanded) { + builder.OpenElement(2, "div"); + builder.AddAttribute(3, "style", "padding-left: 10px;"); + foreach (var child in children) { + builder.AddContent(4, RecursingSpaceChildren(child, parents.Append(space).ToList(), depth + 1)); + } + + builder.CloseElement(); + } + }); + + // Console.WriteLine($"RecursingSpaceChildren::Render - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {renderSw.Elapsed}"); + if (totalSw.ElapsedMilliseconds > 20) + Console.WriteLine($"RecursingSpaceChildren::Total - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {totalSw.Elapsed}"); + // Console.WriteLine($"RecursingSpaceChildren::Total - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {totalSw.Elapsed}"); + return rf; + } + + // private RenderFragment SpaceListEntry(RoomInfo space) { + // return builder => { + // { + // builder.OpenElement(0, "div"); + // builder.AddAttribute(1, "style", "display: block; width: 100%; height: 50px;"); + // builder.AddAttribute(2, "onclick", EventCallback.Factory.Create(this, () => { + // if (OpenedSpaces.Contains(space)) { + // OpenedSpaces.Remove(space); + // } + // else { + // OpenedSpaces.Add(space); + // } + // + // StateHasChanged(); + // })); + // { + // builder.OpenComponent<MxcImage>(5); + // builder.AddAttribute(6, "Homeserver", Data.Homeserver); + // builder.AddAttribute(7, "MxcUri", space.RoomIcon); + // builder.AddAttribute(8, "Circular", true); + // builder.AddAttribute(9, "Width", 32); + // builder.AddAttribute(10, "Height", 32); + // builder.CloseComponent(); + // } + // { + // // room name, ellipsized + // builder.OpenElement(11, "span"); + // builder.AddAttribute(12, "class", "spaceNameEllipsis"); + // builder.AddContent(13, space.RoomName); + // builder.CloseElement(); + // } + // builder.CloseElement(); + // } + // }; + // } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor.css b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor.css diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2SyncContainer.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2SyncContainer.razor new file mode 100644 index 0000000..bbc63eb --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2SyncContainer.razor @@ -0,0 +1,202 @@ +@using LibMatrix.Helpers +@using LibMatrix.Responses +@using MatrixUtils.Abstractions +@using System.Diagnostics +@using System.Diagnostics.CodeAnalysis +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Extensions +@using LibMatrix.Utilities +@using System.Collections.ObjectModel +@using ArcaneLibs +@inject ILogger<RoomsIndex2SyncContainer> logger +<pre>RoomsIndex2SyncContainer</pre> +@foreach (var (name, value) in _statusList) { + <pre>[@name] @value.Status</pre> +} + +@code { + + [Parameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + private SyncHelper syncHelper; + + private Queue<KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure>> queue = new(); + + private ObservableCollection<(string name, ObservableStatus value)> _statusList = new(); + + protected override async Task OnInitializedAsync() { + _statusList.CollectionChanged += (sender, args) => { + StateHasChanged(); + if (args.NewItems is { Count: > 0 }) + foreach (var item in args.NewItems) { + if (item is not (string name, ObservableStatus value)) continue; + value.PropertyChanged += (sender, args) => { + if(value.Show) StateHasChanged(); + }; + } + }; + + while (Data.Homeserver is null) { + await Task.Delay(100); + } + + await SetUpSync(); + } + + private async Task SetUpSync() { + var status = await GetOrAddStatus("Main"); + var syncHelpers = new Dictionary<string, SyncHelper>() { + ["Main"] = new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetBasicRoomInfo), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + } + }; + status.Status = "Initial sync... Checking server filter capability..."; + var syncRes = await syncHelpers["Main"].SyncAsync(); + if (!syncRes.Rooms?.Join?.Any(x => x.Value.State?.Events?.Any(y => y.Type == SpaceChildEventContent.EventId) ?? false) ?? true) { + status.Status = "Initial sync indicates that server supports filters, starting helpers!"; + syncHelpers.Add("SpaceRelations", new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetSpaceRelations), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + }); + + syncHelpers.Add("Profile", new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetOwnMemberEvents), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + }); + } + else status.Status = "Initial sync indicates that server does not support filters, continuing without extra filters!"; + + await HandleSyncResponse(syncRes); + + // profileSyncHelper = new SyncHelper(Homeserver, logger) { + // Timeout = 10000, + // Filter = profileUpdateFilter, + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + // }; + // profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId); + RunQueueProcessor(); + foreach (var helper in syncHelpers) { + Console.WriteLine($"Starting sync loop for {helper.Key}"); + RunSyncLoop(helper.Value, helper.Key); + } + } + + private async Task RunQueueProcessor() { + var status = await GetOrAddStatus("QueueProcessor"); + var statusd = await GetOrAddStatus("QueueProcessor/D", show: false); + while (true) { + await Task.Delay(1000); + try { + var renderTimeSw = Stopwatch.StartNew(); + while (queue.Count == 0) { + var delay = 1000; + Console.WriteLine("Queue is empty, waiting..."); + // Status2 = $"Queue is empty, waiting for {delay}ms..."; + await Task.Delay(delay); + } + + status.Status = $"Queue no longer empty after {renderTimeSw.Elapsed}!"; + renderTimeSw.Restart(); + + int maxUpdates = 5000; + while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) { + var (roomId, roomData) = queueEntry; + statusd.Status = $"Dequeued room {roomId}"; + RoomInfo room; + + if (Data.Rooms.Any(x => x.Room.RoomId == roomId)) { + room = Data.Rooms.First(x => x.Room.RoomId == roomId); + statusd.Status = $"{roomId} already known with {room.StateEvents?.Count ?? 0} state events"; + } + else { + statusd.Status = $"Eencountered new room {roomId}!"; + room = new RoomInfo(Data.Homeserver!.GetRoom(roomId), roomData.State?.Events); + Data.Rooms.Add(room); + } + + if (roomData.State?.Events is { Count: > 0 }) + room.StateEvents!.MergeStateEventLists(roomData.State.Events); + else { + statusd.Status = $"Could not merge state for {room.Room.RoomId} as new data contains no state events!"; + } + + // await Task.Delay(10); + } + + status.Status = $"Got {Data.Rooms.Count} rooms so far! {queue.Count} entries left in processing queue... Parsed last response in {renderTimeSw.Elapsed}"; + + // RenderContents |= queue.Count == 0; + // await Task.Delay(Data.Rooms.Count); + } + catch (Exception e) { + Console.WriteLine("QueueWorker exception: " + e); + } + } + } + + private async Task RunSyncLoop(SyncHelper syncHelper, string name = "Unknown") { + var status = await GetOrAddStatus($"SYNC/{name}"); + status.Status = $"Initial syncing..."; + + var syncs = syncHelper.EnumerateSyncAsync(); + await foreach (var sync in syncs) { + var sw = Stopwatch.StartNew(); + status.Status = $"[{DateTime.Now}] Got {Data.Rooms.Count} rooms so far! {sync.Rooms?.Join?.Count ?? 0} new updates!"; + + await HandleSyncResponse(sync); + status.Status += $"\nProcessed sync in {sw.ElapsedMilliseconds}ms, queue length: {queue.Count}"; + } + } + + private async Task HandleSyncResponse(SyncResponse? sync) { + if (sync?.Rooms?.Join is { Count: > 0 }) + foreach (var joinedRoom in sync.Rooms.Join) + queue.Enqueue(joinedRoom); + + if (sync.Rooms.Leave is { Count: > 0 }) + foreach (var leftRoom in sync.Rooms.Leave) + if (Data.Rooms.Any(x => x.Room.RoomId == leftRoom.Key)) + Data.Rooms.Remove(Data.Rooms.First(x => x.Room.RoomId == leftRoom.Key)); + } + + private SemaphoreSlim _syncLock = new(1, 1); + + private async Task<ObservableStatus> GetOrAddStatus(string name, bool show = true, bool log = true) { + await _syncLock.WaitAsync(); + try { + if (_statusList.Any(x => x.name == name)) + return _statusList.First(x => x.name == name).value; + var status = new ObservableStatus() { + Name = name, + Log = log, + Show = show + }; + _statusList.Add((name, status)); + return status; + } + finally { + _syncLock.Release(); + } + } + + private class ObservableStatus : NotifyPropertyChanged { + private string _status = "Initialising..."; + public string Name { get; set; } = "Unknown"; + public bool Show { get; set; } = true; + public bool Log { get; set; } = true; + + public string Status { + get => _status; + set { + if(SetField(ref _status, value) && Log) + Console.WriteLine($"[{Name}]: {value}"); + } + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor b/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor new file mode 100644 index 0000000..c94d0b0 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor @@ -0,0 +1,158 @@ +@page "/Tools/PolicyListActivity" +@using LibMatrix.EventTypes.Spec.State.Policy +@using System.Diagnostics +@using LibMatrix.RoomTypes +@using LibMatrix.EventTypes.Common + + +@if (RoomData.Count == 0) +{ + <p>Loading...</p> +} +else + foreach (var room in RoomData) + { + <h3>@room.Key</h3> + @foreach (var year in room.Value.OrderBy(x => x.Key)) + { + <h5>@year.Key</h5> + <ActivityGraph Data="@year.Value" GlobalMax="MaxValue" + RLabel="removed" GLabel="new" BLabel="updated policies"> + </ActivityGraph> + } + } + + +@code { + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + public List<GenericRoom> FilteredRooms = new(); + + public Dictionary<DateOnly, ActivityGraph.RGB> TestData { get; set; } = new(); + + public ActivityGraph.RGB MaxValue { get; set; } = new() + { + R = 255, G = 255, B = 255 + }; + + public Dictionary<string, Dictionary<int, Dictionary<DateOnly, ActivityGraph.RGB>>> RoomData { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + var sw = Stopwatch.StartNew(); + await base.OnInitializedAsync(); + Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!; + if (Homeserver is null) return; + + //random test data + for (DateOnly i = new DateOnly(2020, 1, 1); i < new DateOnly(2020, 12, 30); i = i.AddDays(Random.Shared.Next(5))) + { + TestData[i] = new() + { + R = (int)(Random.Shared.NextSingle() * 255), + G = (int)(Random.Shared.NextSingle() * 255), + B = (int)(Random.Shared.NextSingle() * 255) + }; + } + + StateHasChanged(); + // return; + + var rooms = await Homeserver.GetJoinedRooms(); + // foreach (var room in rooms) + // { + // var type = await room.GetRoomType(); + // if (type == "support.feline.policy.lists.msc.v1") + // { + // Console.WriteLine($"{room.RoomId} is policy list by type"); + // FilteredRooms.Add(room); + // } + // else if(await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null) + // { + // Console.WriteLine($"{room.RoomId} is policy list by shortcode"); + // FilteredRooms.Add(room); + // } + // } + var roomFilterTasks = rooms.Select(async room => + { + var type = await room.GetRoomType(); + if (type == "support.feline.policy.lists.msc.v1") + { + Console.WriteLine($"{room.RoomId} is policy list by type"); + return room; + } + else if (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null) + { + Console.WriteLine($"{room.RoomId} is policy list by shortcode"); + return room; + } + + return null; + }).ToList(); + var filteredRooms = await Task.WhenAll(roomFilterTasks); + FilteredRooms.AddRange(filteredRooms.Where(x => x is not null).Cast<GenericRoom>()); + Console.WriteLine($"Filtered {FilteredRooms.Count} rooms in {sw.ElapsedMilliseconds}ms"); + + var roomTasks = FilteredRooms.Select(FetchRoomHistory).ToList(); + await Task.WhenAll(roomTasks); + + Console.WriteLine($"Max value is {MaxValue.R} {MaxValue.G} {MaxValue.B}"); + Console.WriteLine($"Filtered {FilteredRooms.Count} rooms in {sw.ElapsedMilliseconds}ms"); + } + + public async Task FetchRoomHistory(GenericRoom room) + { + var roomName = await room.GetNameOrFallbackAsync(); + if (string.IsNullOrWhiteSpace(roomName)) roomName = room.RoomId; + if (!RoomData.ContainsKey(roomName)) + { + RoomData[roomName] = new(); + } + + //use timeline + var timeline = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 5000); + await foreach (var response in timeline) + { + Console.WriteLine($"Got {response.State.Count} state, {response.Chunk.Count} timeline"); + if (response.State.Count != 0) throw new Exception("Why the hell did we receive state events?"); + foreach (var message in response.Chunk) + { + if (!message.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + //OriginServerTs to datetime + var dt = DateTimeOffset.FromUnixTimeMilliseconds((long)message.OriginServerTs!.Value).DateTime; + var date = new DateOnly(dt.Year, dt.Month, dt.Day); + if (!RoomData[roomName].ContainsKey(date.Year)) + { + RoomData[roomName][date.Year] = new(); + } + + if (!RoomData[roomName][date.Year].ContainsKey(date)) + { + // Console.WriteLine($"Adding {date} to {roomName}"); + RoomData[roomName][date.Year][date] = new(); + } + + var rgb = RoomData[roomName][date.Year][date]; + if (message.RawContent?.Count == 0) rgb.R++; + else if (string.IsNullOrWhiteSpace(message.Unsigned?.ReplacesState)) rgb.G++; + else rgb.B++; + RoomData[roomName][date.Year][date] = rgb; + } + + var max = RoomData.SelectMany(x => x.Value.Values).Aggregate(new ActivityGraph.RGB(), (current, next) => new() + { + R = Math.Max(current.R, next.Average(x => x.Value.R)), + G = Math.Max(current.G, next.Average(x => x.Value.G)), + B = Math.Max(current.B, next.Average(x => x.Value.B)) + }); + MaxValue = new ActivityGraph.RGB( + r: Math.Max(max.R, Math.Max(max.G, max.B)), + g: Math.Max(max.R, Math.Max(max.G, max.B)), + b: Math.Max(max.R, Math.Max(max.G, max.B))); + Console.WriteLine($"Max value is {MaxValue.R} {MaxValue.G} {MaxValue.B}"); + StateHasChanged(); + await Task.Delay(100); + } + } + + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/UserTrace.razor index b3a7487..d78c58a 100644 --- a/MatrixUtils.Web/Pages/Tools/UserTrace.razor +++ b/MatrixUtils.Web/Pages/Tools/UserTrace.razor @@ -80,18 +80,33 @@ Random.Shared.Shuffle(distinctRooms); rooms = new ObservableCollection<GenericRoom>(distinctRooms); rooms.CollectionChanged += (sender, args) => StateHasChanged(); + try { + var stateTasks = rooms.Select(async x => { + for (int i = 0; i < 10; i++) { + try { + return (x, await x.GetMembersListAsync(false)); + } + catch { + // + } + } - var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync(false))).ToAsyncEnumerable(); + return (x, new List<StateEventResponse>().ToFrozenSet()); + }).ToAsyncEnumerable(); - await foreach (var (room, state) in stateTasks) { - roomMembers.Add(room, state); - log.Add($"Got {state.Count} members for {room.RoomId}..."); + await foreach (var (room, state) in stateTasks) { + roomMembers.Add(room, state); + log.Add($"Got {state.Count} members for {room.RoomId}..."); + } + } + catch { + // } log.Add($"Done fetching members!"); - UserIDs.RemoveAll(x=>sessions.Any(y=>y.UserId == x)); - + UserIDs.RemoveAll(x => sessions.Any(y => y.UserId == x)); + StateHasChanged(); Console.WriteLine("Rerendered!"); await base.OnInitializedAsync(); @@ -105,7 +120,6 @@ matches[userId].Add(new() { Event = events.First(x => x.StateKey == userId && x.Type == RoomMemberEventContent.EventId), Room = room, - }); } } @@ -132,6 +146,7 @@ private class Matches { public GenericRoom Room; + public StateEventResponse Event; // public } diff --git a/MatrixUtils.Web/Pages/User/DMManager.razor b/MatrixUtils.Web/Pages/User/DMManager.razor index df5cd6b..80bf3b2 100644 --- a/MatrixUtils.Web/Pages/User/DMManager.razor +++ b/MatrixUtils.Web/Pages/User/DMManager.razor @@ -2,6 +2,7 @@ @using LibMatrix.EventTypes.Spec.State @using LibMatrix.Responses @using MatrixUtils.Abstractions +@using LibMatrix <h3>Direct Messages</h3> <hr/> @@ -36,11 +37,19 @@ 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 userTasks = dms.Select(async kv => { + var (userId, rooms) = kv; var roomList = new List<RoomInfo>(); - DMRooms.Add(await Homeserver.GetProfileAsync(userId), roomList); + UserProfileResponse? profile = null; + try { + profile = await Homeserver.GetProfileAsync(userId); + } + catch (MatrixException e) { + if (e is { ErrorCode: "M_UNKNOWN" }) profile = new UserProfileResponse() { DisplayName = $"{userId}: {e.Error}" }; + } + foreach (var room in rooms) { - var roomInfo = new RoomInfo() { Room = Homeserver.GetRoom(room) }; + var roomInfo = new RoomInfo(Homeserver.GetRoom(room)); roomList.Add(roomInfo); roomInfo.StateEvents.Add(new() { Type = RoomNameEventContent.EventId, @@ -50,8 +59,13 @@ RoomId = room, Sender = null, EventId = null }); } + + DMRooms.Add(profile ?? new() { DisplayName = userId }, roomList); StateHasChanged(); - } + }).ToList(); + + await Task.WhenAll(userTasks); + await Task.Delay(500); StateHasChanged(); Status = null; diff --git a/MatrixUtils.Web/Pages/User/DMSpace.razor b/MatrixUtils.Web/Pages/User/DMSpace.razor index 519cfff..e3dba30 100644 --- a/MatrixUtils.Web/Pages/User/DMSpace.razor +++ b/MatrixUtils.Web/Pages/User/DMSpace.razor @@ -1,11 +1,14 @@ @page "/User/DMSpace/Setup" @using LibMatrix +@using LibMatrix.Responses +@using MatrixUtils.Abstractions @using MatrixUtils.LibDMSpace @using MatrixUtils.LibDMSpace.StateEvents @using MatrixUtils.Web.Pages.User.DMSpaceStages +@using System.Text.Json.Serialization <h3>DM Space Management</h3> <hr/> -<CascadingValue Value="@DmSpace"> +<CascadingValue Value="@SetupData"> @switch (Stage) { case -1: <p>Initialising...</p> @@ -41,36 +44,29 @@ } } - public AuthenticatedHomeserverGeneric? Homeserver { get; set; } - public DMSpaceConfiguration? DmSpaceConfiguration { get; set; } - - [Parameter] - public DMSpace? DmSpace { get; set; } + public DMSpace? DMSpaceRootPage { get; set; } protected override async Task OnInitializedAsync() { if (NavigationManager.Uri.Contains("?stage=")) { - NavigationManager.NavigateTo("/User/DMSpace", true); + NavigationManager.NavigateTo("/User/DMSpace/Setup", true); } - DmSpace = this; - Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate(); - if (Homeserver is null) return; + DMSpaceRootPage = this; + SetupData.Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate(); + if (SetupData.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); + SetupData.DmSpaceConfiguration = await SetupData.Homeserver.GetAccountDataAsync<DMSpaceConfiguration>("gay.rory.dm_space"); + var room = SetupData.Homeserver.GetRoom(SetupData.DmSpaceConfiguration.DMSpaceId); + await room.GetStateAsync<DMSpaceInfo>(DMSpaceInfo.EventId); Stage = 1; } catch (MatrixException e) { - if (e.ErrorCode == "M_NOT_FOUND") { + if (e.ErrorCode is "M_NOT_FOUND" or "M_FORBIDDEN") { Stage = 0; - DmSpaceConfiguration = new(); + SetupData.DmSpaceConfiguration = new(); } else throw; } - catch (Exception e) { - throw; - } finally { StateHasChanged(); } @@ -82,4 +78,27 @@ await base.OnParametersSetAsync(); } + public DMSpaceSetupData SetupData { get; set; } = new(); + + public class DMSpaceSetupData { + + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + public DMSpaceConfiguration? DmSpaceConfiguration { get; set; } + + public DMSpaceInfo? DmSpaceInfo { get; set; } = new(); + + public Dictionary<string, RoomInfo>? Spaces; + + public Dictionary<UserProfileWithId, List<RoomInfo>>? DMRooms; + + public RoomInfo? DMSpaceRoomInfo { get; set; } + + + public class UserProfileWithId : UserProfileResponse { + [JsonIgnore] + public string Id { get; set; } + } + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor index 49fd5b4..5f6508c 100644 --- a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor +++ b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor @@ -4,7 +4,7 @@ <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> +<LinkButton href="/User/DMSpace/Setup?stage=1">Get started</LinkButton> @code { diff --git a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor index 6131617..2176467 100644 --- a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor +++ b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor @@ -6,30 +6,40 @@ @using MatrixUtils.LibDMSpace.StateEvents @using Microsoft.Extensions.Primitives @using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using MatrixUtils.Abstractions <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> +@if (SetupData is not null) { + if (SetupData.Spaces is not null) { + <p> + Selected space: + <InputSelect @bind-Value="SetupData.DmSpaceConfiguration.DMSpaceId"> + <option value="">New space</option> + @foreach (var (id, roomInfo) in SetupData.Spaces) { + <option value="@id">@roomInfo.RoomName</option> + } + </InputSelect> + </p> + <p> + <InputCheckbox @bind-Value="SetupData.DmSpaceInfo.LayerByUser"></InputCheckbox> + Create sub-spaces per user + </p> + + <br/> + <LinkButton OnClick="@Disband" Color="#FF0000">Disband</LinkButton> + <LinkButton OnClick="@Execute">Next</LinkButton> + } + else { + <p>Discovering spaces, please wait...</p> + } } else { - <b>Error: DmSpaceConfiguration is null!</b> + <b>Error: Setup data is null!</b> } -<br/> -<LinkButton OnClick="@Execute">Next</LinkButton> @if (!string.IsNullOrWhiteSpace(Status)) { <p>@Status</p> @@ -45,84 +55,97 @@ else { } } - private Dictionary<string, string> spaces = new() { { "", "New space" } }; private string? _status; [CascadingParameter] - public DMSpace? DmSpace { get; set; } + public DMSpace.DMSpaceSetupData SetupData { get; set; } - public DMSpaceInfo? DmSpaceInfo { get; set; } = new(); + SemaphoreSlim _semaphoreSlim = new(1, 1); protected override async Task OnInitializedAsync() { - await base.OnInitializedAsync(); - } - - SemaphoreSlim _semaphoreSlim = new(1, 1); - protected override async Task OnParametersSetAsync() { - if (DmSpace is null) + if (SetupData 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!"; + + Dictionary<string, RoomInfo> spaces = []; + SetupData.DmSpaceConfiguration ??= new(); + + Status = "Looking for spaces..."; + var userRoomsEnum = SetupData.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.roomInfo); + + SetupData.Spaces = spaces; + + 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; + if (string.IsNullOrWhiteSpace(SetupData!.DmSpaceConfiguration!.DMSpaceId)) { + var createRoomRequest = CreateRoomRequest.CreatePrivate(SetupData.Homeserver!, "Direct Messages"); + createRoomRequest.CreationContentBaseType.Type = "m.space"; + SetupData.DmSpaceConfiguration.DMSpaceId = (await SetupData.Homeserver!.CreateRoom(createRoomRequest)).RoomId; } - await DmSpace.Homeserver!.SetAccountDataAsync(DMSpaceConfiguration.EventId, DmSpace.DmSpaceConfiguration); - var space = DmSpace.Homeserver.GetRoom(DmSpace.DmSpaceConfiguration.DMSpaceId); - await space.SendStateEventAsync(DMSpaceInfo.EventId, DmSpaceInfo); + + await SetupData.Homeserver!.SetAccountDataAsync(DMSpaceConfiguration.EventId, SetupData.DmSpaceConfiguration); + var space = SetupData.Homeserver.GetRoom(SetupData.DmSpaceConfiguration.DMSpaceId); + await space.SendStateEventAsync(DMSpaceInfo.EventId, SetupData.DmSpaceInfo); + SetupData.DMSpaceRoomInfo = new RoomInfo(space); + await SetupData.DMSpaceRoomInfo.FetchAllStateAsync(); NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=2"); } - public async Task<(string id, string name)?> GetFeasibleSpaces(GenericRoom room) { + public async Task<(string id, RoomInfo roomInfo)?> GetFeasibleSpaces(GenericRoom room) { try { - var pls = await room.GetPowerLevelsAsync(); - if (!pls.UserHasStatePermission(DmSpace.Homeserver.WhoAmI.UserId, "m.space.child")) { + var ri = new RoomInfo(room); + + await foreach(var evt in room.GetFullStateAsync()) + ri.StateEvents.Add(evt); + + var powerLevels = (await ri.GetStateEvent(RoomPowerLevelEventContent.EventId)).TypedContent as RoomPowerLevelEventContent; + if (!powerLevels.UserHasStatePermission(SetupData.Homeserver.WhoAmI.UserId, SpaceChildEventContent.EventId)) { 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; + + Status = $"Found viable space: {ri.RoomName}"; + if (!string.IsNullOrWhiteSpace(SetupData.DmSpaceConfiguration!.DMSpaceId)) { + if (await room.GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) is { } dsi) { + SetupData.DmSpaceConfiguration.DMSpaceId = room.RoomId; + SetupData.DmSpaceInfo = dsi; + Console.WriteLine(dsi.ToJson(ignoreNull: true)); } } - return (room.RoomId, roomName); + + if (ri.RoomName == room.RoomId) + ri.RoomName = await room.GetNameOrFallbackAsync(); + + return (room.RoomId, ri); } 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; } + private async Task Disband() { + var space = new DMSpaceRoom(SetupData.Homeserver, SetupData.DmSpaceConfiguration.DMSpaceId); + await space.DisbandDMSpace(); + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor index 5a53347..a70e9c5 100644 --- a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor +++ b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor @@ -17,18 +17,23 @@ <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> +@if (SetupData is not null) { + if (SetupData.DMRooms is { Count: > 0 }) { + @foreach (var (userId, room) in SetupData.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 { + <p>DM room list is loading, please wait...</p> + } } else { - <b>Error: DmSpaceConfiguration is null!</b> + <b>Error: DMSpaceRootPage is null!</b> } <br/> @@ -88,26 +93,21 @@ else { private RoomInfo? _dmToReassign; [CascadingParameter] - public DMSpace? DmSpace { get; set; } + public DMSpace.DMSpaceSetupData SetupData { 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(); - } + private Dictionary<RoomInfo, List<DMSpace.DMSpaceSetupData.UserProfileWithId>> duplicateDmRooms { get; set; } = new(); + private Dictionary<RoomInfo, List<DMSpace.DMSpaceSetupData.UserProfileWithId>> roomMembers { get; set; } = new(); SemaphoreSlim _semaphore = new(1, 1); - protected override async Task OnParametersSetAsync() { - if (DmSpace is null) + protected override async Task OnInitializedAsync() { + if (SetupData is null) return; await _semaphore.WaitAsync(); DmToReassign = null; - var hs = DmSpace.Homeserver; + var hs = SetupData.Homeserver; Status = "Loading DM list from account data..."; - var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + var dms = await SetupData.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) { @@ -116,18 +116,22 @@ else { 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(); + + dms.RemoveAll((x, y) => y is { Count: 0 }); + await SetupData.Homeserver.SetAccountDataAsync("m.direct", dms); Status = "DM list optimised, fetching info..."; + + SetupData.DMRooms = new Dictionary<DMSpace.DMSpaceSetupData.UserProfileWithId, List<RoomInfo>>(); + var results = dms.Select(async x => { var (userId, rooms) = x; - UserProfileWithId userProfile; + DMSpace.DMSpaceSetupData.UserProfileWithId userProfile; try { - var profile = await DmSpace.Homeserver.GetProfileAsync(userId); + var profile = await SetupData.Homeserver.GetProfileAsync(userId); userProfile = new() { AvatarUrl = profile.AvatarUrl, Id = userId, @@ -141,32 +145,35 @@ else { 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(); + // StateHasChanged(); }).ToAsyncEnumerable(); await foreach (var res in results) { - dmRooms.Add(res.userProfile, res.roomList); - // Status = $"Listed {dmRooms.Count} users"; + SetupData.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) { + var duplicateDmRoomIds = new Dictionary<string, List<DMSpace.DMSpaceSetupData.UserProfileWithId>>(); + foreach (var (user, rooms) in SetupData.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); + duplicateDmRooms.Add(SetupData.DMRooms.First(x => x.Value.Any(x => x.Room.RoomId == roomId)).Value.First(x => x.Room.RoomId == roomId), users); } - // StateHasChanged(); + // StateHasChanged(); Status = null; await base.OnParametersSetAsync(); } @@ -176,34 +183,29 @@ else { } private async Task<RoomInfo> GetRoomInfo(GenericRoom room) { - var roomInfo = new RoomInfo() { - Room = room - }; + var roomInfo = new RoomInfo(room); + await roomInfo.FetchAllStateAsync(); roomMembers[roomInfo] = new(); - roomInfo.CreationEventContent = await room.GetCreateEventAsync(); - try { - roomInfo.RoomName = await room.GetNameAsync(); - } - catch { } + // roomInfo.CreationEventContent = await room.GetCreateEventAsync(); + + if(roomInfo.RoomName == room.RoomId) + try { + roomInfo.RoomName = await room.GetNameOrFallbackAsync(); + } + catch { } var membersEnum = room.GetMembersEnumerableAsync(true); await foreach (var member in membersEnum) if (member.TypedContent is RoomMemberEventContent memberEvent) 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; } @@ -214,29 +216,25 @@ else { } private async Task SetRoomAssignment(string roomId, string userId) { - var hs = DmSpace.Homeserver; + var hs = SetupData.Homeserver; Status = "Loading DM list from account data..."; - var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + var dms = await SetupData.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)) + + 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); + dms.RemoveAll((x, y) => y is { Count: 0 }); + await SetupData.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/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor index 9307f6a..865e956 100644 --- a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor +++ b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor @@ -18,15 +18,15 @@ <p>@Status</p> } -@if (DmSpace is not null) { - @if (dmSpaceInfo is not null && dmSpaceRoomInfo is not null) { +@if (SetupData is not null) { + @if (SetupData.DMSpaceRoomInfo is not null) { <p> - <InputCheckbox @bind-Value="dmSpaceInfo.LayerByUser"></InputCheckbox> + <InputCheckbox @bind-Value="SetupData.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)) { + @if (!SetupData.DmSpaceInfo.LayerByUser) { + <RoomListItem RoomInfo="@SetupData.DMSpaceRoomInfo"></RoomListItem> + @foreach (var (userId, room) in SetupData.DMRooms.OrderBy(x => x.Key.DisplayName)) { @foreach (var roomInfo in room) { <div style="margin-left: 32px;"> <RoomListItem RoomInfo="@roomInfo"></RoomListItem> @@ -35,10 +35,16 @@ } } else { - <RoomListItem RoomInfo="@dmSpaceRoomInfo"></RoomListItem> - @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.RoomName)) { + <RoomListItem RoomInfo="@SetupData.DMSpaceRoomInfo"></RoomListItem> + @foreach (var (user, room) in SetupData.DMRooms.OrderBy(x => x.Key.DisplayName)) { <div style="margin-left: 32px;"> - <RoomListItem RoomInfo="@userId"></RoomListItem> + @{ + RoomInfo fakeRoom = new(SetupData.DMSpaceRoomInfo.Room) { + RoomName = user.DisplayName ?? user.Id, + RoomIcon = user.AvatarUrl + }; + } + <RoomListItem RoomInfo="@fakeRoom"></RoomListItem> </div> @foreach (var roomInfo in room) { <div style="margin-left: 64px;"> @@ -49,11 +55,11 @@ } } else { - <b>Error: dmSpaceInfo is null!</b> + <b>Error: SetupData.DMSpaceRoomInfo is null!</b> } } else { - <b>Error: DmSpaceConfiguration is null!</b> + <b>Error: DMSpaceRootPageConfiguration is null!</b> } <br/> @@ -72,83 +78,75 @@ else { 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(); - } + public DMSpace.DMSpaceSetupData SetupData { get; set; } SemaphoreSlim _semaphore = new(1, 1); - protected override async Task OnParametersSetAsync() { - if (DmSpace is null) + protected override async Task OnInitializedAsync() { + if (SetupData 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(); + var hs = SetupData.Homeserver; + // var dmSpaceRoom = new DMSpaceRoom(hs, SetupData.DmSpaceConfiguration.DMSpaceId); + // SetupData. + // 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 SetupData.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); 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); - } + // var results = dms.Select(async x => { + // var (userId, rooms) = x; + // UserProfileWithId userProfile; + // try { + // var profile = await SetupData.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); + // } + await SetupData.DMSpaceRoomInfo!.FetchAllStateAsync(); _semaphore.Release(); Status = null; await base.OnParametersSetAsync(); } private async Task Execute() { - var hs = DmSpace.Homeserver; - var dmSpaceRoom = new DMSpaceRoom(hs, DmSpace.DmSpaceConfiguration.DMSpaceId); + var hs = SetupData.Homeserver; + var dmSpaceRoom = new DMSpaceRoom(hs, SetupData.DmSpaceConfiguration!.DMSpaceId!); + await dmSpaceRoom.ImportNativeDMs(); NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=3"); } private async Task<RoomInfo> GetRoomInfo(GenericRoom room) { - var roomInfo = new RoomInfo() { - Room = room - }; + var roomInfo = new RoomInfo(room); var roomMembers = new List<UserProfileWithId>(); roomInfo.CreationEventContent = await room.GetCreateEventAsync(); try { @@ -168,12 +166,14 @@ else { 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; } diff --git a/MatrixUtils.Web/Pages/User/Profile.razor b/MatrixUtils.Web/Pages/User/Profile.razor index 79b83ae..129f706 100644 --- a/MatrixUtils.Web/Pages/User/Profile.razor +++ b/MatrixUtils.Web/Pages/User/Profile.razor @@ -110,8 +110,7 @@ var room = Homeserver.GetRoom(roomId); var roomNameTask = room.GetNameOrFallbackAsync(); var roomIconTask = room.GetAvatarUrlAsync(); - var roomInfo = new RoomInfo() { - Room = room, + var roomInfo = new RoomInfo(room) { OwnMembership = roomProfile }; try { diff --git a/MatrixUtils.Web/Shared/ActivityGraph.razor b/MatrixUtils.Web/Shared/ActivityGraph.razor new file mode 100644 index 0000000..51fb539 --- /dev/null +++ b/MatrixUtils.Web/Shared/ActivityGraph.razor @@ -0,0 +1,148 @@ +@using System.Drawing +@using System.Runtime.InteropServices +@using System.Diagnostics + +@if (Data is { Count: > 0 }) +{ + @* 12*5=60 *@ + <div style="display: grid; grid-template-columns: 35px repeat(60, 1.5em); grid-template-rows: 1.5em repeat(7, 1.5em); gap: 0;"> + @* row 0: month labels with colspan *@ + @* @foreach (var month in Enumerable.Range(1, 12)) *@ + @* { *@ + @* <div style="grid-row: 1; grid-column: @((int)(month * 4.3) + 1);"> *@ + @* <span aria-hidden="true">@(new DateTime(2021, month, 1).ToString("MMM")[..3])</span> *@ + @* </div> *@ + @* } *@ + + @* column 0: day labels *@ + @* @for (var i = 0; i < 7; i++) *@ + @* { *@ + @* <div style="text-align: left; grid-column: 1; grid-row: @(i + 2)"> *@ + @* @(((DayOfWeek)i).ToString()[..3]) *@ + @* </div> *@ + @* } *@ + + + <div style="grid-row: 1; grid-column: 5;">Jan</div> + <div style="grid-row: 1; grid-column: 9;">Feb</div> + <div style="grid-row: 1; grid-column: 13;">Mar</div> + <div style="grid-row: 1; grid-column: 18;">Apr</div> + <div style="grid-row: 1; grid-column: 22;">May</div> + <div style="grid-row: 1; grid-column: 26;">Jun</div> + <div style="grid-row: 1; grid-column: 31;">Jul</div> + <div style="grid-row: 1; grid-column: 35;">Aug</div> + <div style="grid-row: 1; grid-column: 39;">Sep</div> + <div style="grid-row: 1; grid-column: 44;">Oct</div> + <div style="grid-row: 1; grid-column: 48;">Nov</div> + <div style="grid-row: 1; grid-column: 52;">Dec</div> + <div style="text-align: left; grid-column: 1; grid-row: 2">Sun</div> + <div style="text-align: left; grid-column: 1; grid-row: 3">Mon</div> + <div style="text-align: left; grid-column: 1; grid-row: 4">Tue</div> + <div style="text-align: left; grid-column: 1; grid-row: 5">Wed</div> + <div style="text-align: left; grid-column: 1; grid-row: 6">Thu</div> + <div style="text-align: left; grid-column: 1; grid-row: 7">Fri</div> + <div style="text-align: left; grid-column: 1; grid-row: 8">Sat</div> + + + @* pad activity cell dates... *@ + <div style="grid-column: 2; grid-row: 2 / span @((int)(new DateOnly(Data.Keys.First().Year, 1, 1).DayOfWeek));"></div> + + @* the actual activity cells *@ + + @code{ + bool needsBorder = false; + } + + @for (DateOnly date = new DateOnly(Data.Keys.First().Year, 1, 1); date <= new DateOnly(Data.Keys.First().Year, 1, 1).AddYears(1).AddDays(-1); date = date.AddDays(1)) + { + var hasData = Data.TryGetValue(date, out var color); + var needsTopBorder = date.Day == 1 && date.Month != 1 && date.DayOfWeek != DayOfWeek.Sunday; + if (date.DayOfWeek == DayOfWeek.Sunday) + needsBorder = date.AddDays(7).Day <= 7 && date.Month != 12; + var needsLeftBorder = date.Day <= 7; + + <div class="activity-cell-container" + style="grid-row: @((int)date.DayOfWeek + 2); border-@(needsLeftBorder ? "left" : "right"): @(needsBorder ? "2px solid white" : "none"); border-top: @(needsTopBorder ? "2px solid white" : "none");"> + @if (hasData) + { + <div class="activity-cell" + style="background-color: rgb(@(color.R / GlobalMax.R * 255), @(color.G / GlobalMax.G * 255), @(color.B / GlobalMax.B * 255));" + title="@($"{color.R} {RLabel}, {color.G} {GLabel}, and {color.B} {BLabel} on {date.ToString("D")}")"> + </div> + } + else + { + <div class="activity-cell" + title="@($"No data on {date.ToString("D")}")"> + </div> + } + </div> + } + </div> +} + + +@code { + private Dictionary<DateOnly, RGB> _data = new(); + private RGB? _globalMax = null; + + [Parameter] + public Dictionary<DateOnly, RGB> Data + { + get => _data; + set + { + // var sw = Stopwatch.StartNew(); + if (value is not { Count: > 0 }) return; + // Console.WriteLine($"Recalculating activity graph ({value.Count} datapoints)..."); + + + // var year = (int)value.Keys.Average(x => x.Year); + // value = value + // .Where(x => x.Key.Year == year) + // .OrderBy(x => x.Key) + // .ToDictionary(x => x.Key, x => x.Value); + + _data = value; + // Console.WriteLine($"Recalculated activity graph in {sw.Elapsed}"); + // StateHasChanged(); + } + } + + [Parameter] + public RGB GlobalMax + { + get + { + if (_globalMax is not null) return _globalMax.Value; + if (Data is not { Count: > 0 }) return new RGB() { R = 255, G = 255, B = 255 }; + return new RGB() + { + R = Data.Values.Max(x => x.R), + G = Data.Values.Max(x => x.G), + B = Data.Values.Max(x => x.B) + }; + } + set => _globalMax = value; + } + + [Parameter] public string RLabel { get; set; } = "R"; + [Parameter] public string GLabel { get; set; } = "G"; + [Parameter] public string BLabel { get; set; } = "B"; + + [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * 3, Pack = 1)] + public struct RGB() + { + public float R = 0; + public float G = 0; + public float B = 0; + + public RGB(float r, float g, float b) : this() + { + R = r; + G = g; + B = b; + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/ActivityGraph.razor.css b/MatrixUtils.Web/Shared/ActivityGraph.razor.css new file mode 100644 index 0000000..d8e543c --- /dev/null +++ b/MatrixUtils.Web/Shared/ActivityGraph.razor.css @@ -0,0 +1,16 @@ +.activity-cell-container { + width: 100%; + height: 100%; + align-content: center; + justify-content: center; +} + +.activity-cell { + width: 85%; + height: 85%; + border-radius: 5px; +} + +.day-label { + grid-column: 1; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/MainLayout.razor b/MatrixUtils.Web/Shared/MainLayout.razor index d8bf411..41c3d69 100644 --- a/MatrixUtils.Web/Shared/MainLayout.razor +++ b/MatrixUtils.Web/Shared/MainLayout.razor @@ -8,8 +8,8 @@ <main> <div class="top-row px-4"> <PortableDevTools></PortableDevTools> - <a href="https://cgit.rory.gay/matrix/MatrixRoomUtils.git/" target="_blank">Git</a> - <a href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support" target="_blank">Matrix</a> + <a style="color: #ccc; text-decoration: underline" href="https://cgit.rory.gay/matrix/MatrixRoomUtils.git/" target="_blank">Git</a> + <a style="color: #ccc; text-decoration: underline" href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support" target="_blank">Matrix</a> </div> <article class="Content px-4"> diff --git a/MatrixUtils.Web/Shared/MxcImage.razor b/MatrixUtils.Web/Shared/MxcImage.razor index f31c19f..e651c3f 100644 --- a/MatrixUtils.Web/Shared/MxcImage.razor +++ b/MatrixUtils.Web/Shared/MxcImage.razor @@ -30,6 +30,7 @@ StateHasChanged(); } } + [Parameter] public RemoteHomeserver? Homeserver { get; set; } @@ -41,7 +42,7 @@ } } - private string StyleString => $"{Style} {(Circular ? "border-radius: 50%;" : "")} {(Width.HasValue ? $"width: {Width}px;" : "")} {(Height.HasValue ? $"height: {Height}px;" : "")}"; + private string StyleString => $"{Style} {(Circular ? "border-radius: 50%;" : "")} {(Width.HasValue ? $"width: {Width}px;" : "")} {(Height.HasValue ? $"height: {Height}px;" : "")} object-fit: cover;"; private static readonly string Prefix = "mxc://"; private static readonly int PrefixLength = Prefix.Length; diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor index 9c481e3..6954990 100644 --- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor @@ -48,9 +48,7 @@ if (Breadcrumbs.Contains(room.RoomId)) continue; var roomInfo = KnownRooms.FirstOrDefault(x => x.Room.RoomId == room.RoomId); if (roomInfo is null) { - roomInfo = new RoomInfo() { - Room = room - }; + roomInfo = new RoomInfo(room); KnownRooms.Add(roomInfo); } if(joinedRooms.Any(x=>x.RoomId == room.RoomId)) diff --git a/MatrixUtils.Web/Shared/UserListItem.razor b/MatrixUtils.Web/Shared/UserListItem.razor index 525296e..daa9c9e 100644 --- a/MatrixUtils.Web/Shared/UserListItem.razor +++ b/MatrixUtils.Web/Shared/UserListItem.razor @@ -2,8 +2,9 @@ @using LibMatrix.EventTypes.Spec.State @using LibMatrix.Homeservers @using LibMatrix.Responses +@using ArcaneLibs <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)"/> + <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%;" src="@(string.IsNullOrWhiteSpace(User?.AvatarUrl) ? _identiconGenerator.GenerateAsDataUri(UserId) : User.AvatarUrl)"/> <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px;">@User?.DisplayName</span> <div style="display: inline-block;"> @@ -27,6 +28,8 @@ private AuthenticatedHomeserverGeneric _homeserver = null!; + private SvgIdenticonGenerator _identiconGenerator = new(); + protected override async Task OnInitializedAsync() { _homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); if (_homeserver is null) return; @@ -35,6 +38,7 @@ if (UserId == null) { throw new ArgumentNullException(nameof(UserId)); } + User = await _homeserver.GetProfileAsync(UserId); } diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 4c60728..1abe9e7 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -11,4 +11,4 @@ BASE_DIR=`pwd` rm -rf **/bin/Release cd MatrixUtils.Web dotnet publish -c Release -rsync -raP bin/Release/net8.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/ +rsync --delete -raP bin/Release/net8.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/ |