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/
|