about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-03-26 11:43:17 +0100
committerRory& <root@rory.gay>2025-03-26 11:43:57 +0100
commit41b14d6621e2579edf55ccf83ca563cb31d09a2d (patch)
tree8bd74f175fef1f4d8edabc324f15aa4f921078e2
parentWork on README, add license (diff)
downloadMatrixAntiDmSpam-41b14d6621e2579edf55ccf83ca563cb31d09a2d.tar.xz
Simplify invite store, use multiple threads for evaluating policies to scale beyond unreasonable policy counts
-rw-r--r--.gitignore3
m---------LibMatrix0
-rw-r--r--MatrixAntiDmSpam.sln.DotSettings.user3
-rw-r--r--MatrixAntiDmSpam/InviteHandler.cs140
-rw-r--r--MatrixAntiDmSpam/InviteStore.cs15
-rw-r--r--MatrixAntiDmSpam/MatrixAntiDmSpam.csproj4
-rw-r--r--MatrixAntiDmSpam/PolicyExecutor.cs150
-rw-r--r--MatrixAntiDmSpam/PolicyListFetcher.cs76
-rw-r--r--MatrixAntiDmSpam/PolicyStore.cs29
-rw-r--r--MatrixAntiDmSpam/Program.cs3
-rw-r--r--MatrixAntiDmSpam/Properties/launchSettings.json7
11 files changed, 209 insertions, 221 deletions
diff --git a/.gitignore b/.gitignore

index add57be..f798f61 100644 --- a/.gitignore +++ b/.gitignore
@@ -2,4 +2,5 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +appsettings.Local.json diff --git a/LibMatrix b/LibMatrix -Subproject 14931ea211415ce504152945d75eecb1354f564 +Subproject ff13e64dca922550eb6a955de0690d841590a9b diff --git a/MatrixAntiDmSpam.sln.DotSettings.user b/MatrixAntiDmSpam.sln.DotSettings.user
index fe3cfa4..73d444d 100644 --- a/MatrixAntiDmSpam.sln.DotSettings.user +++ b/MatrixAntiDmSpam.sln.DotSettings.user
@@ -4,4 +4,5 @@ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F65_003Fb77a719c_003FList_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObjectExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8fd5e96d6574456095123be1ecfbdfa914200_003Fe2_003F3561a383_003FObjectExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionHostedServiceExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff13297a632424a6abffea4dd75a36a75d128_003Ff9_003Fb35aae11_003FServiceCollectionHostedServiceExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> - <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F86_003F8b4aa64e_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> \ No newline at end of file + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F86_003F8b4aa64e_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATaskAwaiter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F10_003F04572d58_003FTaskAwaiter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> \ No newline at end of file diff --git a/MatrixAntiDmSpam/InviteHandler.cs b/MatrixAntiDmSpam/InviteHandler.cs
index 2a5d17b..26a88c7 100644 --- a/MatrixAntiDmSpam/InviteHandler.cs +++ b/MatrixAntiDmSpam/InviteHandler.cs
@@ -1,113 +1,91 @@ using System.Text.Json; using ArcaneLibs; using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Helpers; -using LibMatrix.Utilities.Bot.Services; +using LibMatrix.RoomTypes; +using LibMatrix.Utilities.Bot.Interfaces; namespace MatrixAntiDmSpam; -public class InviteHandler(ILogger<InviteHandler> logger, AntiDmSpamConfiguration config, InviteStore inviteStore) : InviteHandlerHostedService.IInviteHandler { - public async Task HandleInviteAsync(InviteHandlerHostedService.InviteEventArgs invite) { - // logger.LogInformation("Received invite to room {}", invite.RoomId); - await LogInvite(invite); - await inviteStore.AddInviteAsync(invite); - } +public class RoomInviteHandler(ILogger<RoomInviteHandler> logger, AntiDmSpamConfiguration config) : IRoomInviteHandler { + public List<RoomInviteContext> Invites { get; } = []; - private async Task LogInvite(InviteHandlerHostedService.InviteEventArgs invite) { - if (string.IsNullOrWhiteSpace(config.LogRoom)) return; - var logRoom = invite.Homeserver.GetRoom(config.LogRoom); - var inviterName = await GetInviterNameAsync(invite); - string roomName = await GetRoomNameAsync(invite); + public List<Func<RoomInviteContext, Task>> OnInviteReceived { get; set; } = []; - logger.LogInformation("Inviter: {}, Room: {}", inviterName, roomName); + private GenericRoom? LogRoom { get; set; } - var message = new MessageBuilder() - .WithBody("Received invite to ").WithMention(invite.RoomId, roomName).WithBody(" from ").WithMention(invite.MemberEvent.Sender!, inviterName) - .Build(); + public async Task HandleInviteAsync(RoomInviteContext invite) { + if (!string.IsNullOrWhiteSpace(config.LogRoom)) + LogRoom = invite.Homeserver.GetRoom(config.LogRoom); - // TODO: can we filter this somehow to stay within event size limits? - // var serialisedInviteData = JsonNode.Parse(invite.InviteData.ToJson(ignoreNull: true)); - // message.AdditionalData!["gay.rory.invite_logger.invite_data"] = serialisedInviteData!; + Invites.Add(invite); + await LogInvite(invite); + foreach (var handler in OnInviteReceived) { + try { + await handler.Invoke(invite); + } + catch (Exception e) { + if (!string.IsNullOrWhiteSpace(config.LogRoom)) { + var logRoom = invite.Homeserver.GetRoom(config.LogRoom); + await logRoom.SendMessageEventAsync( + new MessageBuilder() + .WithBody($"Failed to execute invite handler {handler}...").WithNewline() + .WithCollapsibleSection("Stack trace", msb => msb.WithCodeBlock(e.ToString(), "cs")) + .Build() + ); + } + } + } + } + private async Task LogInvite(RoomInviteContext invite) { + logger.LogInformation("Received invite to {} from {}", invite.RoomId, invite.MemberEvent.Sender); + + if (LogRoom is null) return; var inviteData = invite.InviteData.ToJsonUtf8Bytes(ignoreNull: true); - var inviteDataFileUri = await invite.Homeserver.UploadFile(invite.RoomId + ".json", inviteData, "application/json"); - logger.LogInformation("Uploaded invite data ({}) to {}", Util.BytesToString(inviteData.Length), inviteDataFileUri); - // Dictionary<string, JsonElement> - message.AdditionalData!["gay.rory.invite_logger.invite_data_uri"] = JsonDocument.Parse($"\"{inviteDataFileUri}\"").RootElement; + var inviterNameTask = invite.TryGetInviterNameAsync(); + var roomNameTask = invite.TryGetRoomNameAsync(); + var inviteDataFileUriTask = invite.Homeserver.UploadFile(invite.RoomId + ".json", inviteData, "application/json"); + logger.LogInformation("Uploaded invite data ({}) to {}", Util.BytesToString(inviteData.Length), await inviteDataFileUriTask); + + await Task.WhenAll(inviterNameTask, roomNameTask, inviteDataFileUriTask); - await logRoom.SendMessageEventAsync(message); + var message = new MessageBuilder() + .WithBody("Received invite to ").WithMention(invite.RoomId, await roomNameTask).WithBody(" from ").WithMention(invite.MemberEvent.Sender!, await inviterNameTask) + .Build(); + + message.AdditionalData!["gay.rory.invite_logger.invite_data_uri"] = JsonDocument.Parse($"\"{await inviteDataFileUriTask}\"").RootElement; + + await LogRoom.SendMessageEventAsync(message); if (config.LogInviteDataAsFile) { - await logRoom.SendMessageEventAsync(new() { + await LogRoom.SendMessageEventAsync(new() { MessageType = "m.file", Body = invite.RoomId + ".json", FileName = invite.RoomId + ".json", - Url = inviteDataFileUri, + Url = await inviteDataFileUriTask, FileInfo = new() { Size = inviteData.Length, MimeType = "application/json" } }); } } - private async Task<string> GetInviterNameAsync(InviteHandlerHostedService.InviteEventArgs invite) { - var name = invite.InviteData.InviteState?.Events? - .FirstOrDefault(evt => evt is { Type: RoomMemberEventContent.EventId } && evt.StateKey == invite.MemberEvent.Sender)? - .ContentAs<RoomMemberEventContent>()?.DisplayName; - - if (!string.IsNullOrWhiteSpace(name)) - return name; - - try { - await invite.Homeserver.GetProfileAsync(invite.MemberEvent.Sender!); - } - catch { - //ignored - } - - return invite.MemberEvent.Sender!; - } - - private async Task<string> GetRoomNameAsync(InviteHandlerHostedService.InviteEventArgs invite) { - // try get room name from invite state - var name = invite.InviteData.InviteState?.Events? - .FirstOrDefault(evt => evt is { Type: RoomNameEventContent.EventId, StateKey: "" })? - .ContentAs<RoomNameEventContent>()?.Name; - - if (!string.IsNullOrWhiteSpace(name)) - return name; - - // try get room alias - var alias = invite.InviteData.InviteState?.Events? - .FirstOrDefault(evt => evt is { Type: RoomCanonicalAliasEventContent.EventId, StateKey: "" })? - .ContentAs<RoomCanonicalAliasEventContent>()?.Alias; - - if (!string.IsNullOrWhiteSpace(alias)) - return alias; - - // try get room name via public previews - try { -#pragma warning disable CS0618 // Type or member is obsolete - name = await invite.Homeserver.GetRoom(invite.RoomId).GetNameOrFallbackAsync(); -#pragma warning restore CS0618 // Type or member is obsolete - if (name != invite.RoomId && !string.IsNullOrWhiteSpace(name)) - return name; - } - catch { - //ignored - } + public async Task RejectInvite(RoomInviteContext invite, MessageBuilder reason) { + if (LogRoom is not null) + _ = LogRoom.SendMessageEventAsync(reason.Build()); - // fallback to room alias via public previews try { - alias = (await invite.Homeserver.GetRoom(invite.RoomId).GetCanonicalAliasAsync())?.Alias; - if (!string.IsNullOrWhiteSpace(alias)) - return alias; + await invite.Homeserver.GetRoom(invite.RoomId).LeaveAsync(); } - catch { - //ignored + catch (Exception e) { + if (LogRoom is not null) + await LogRoom.SendMessageEventAsync( + new MessageBuilder().WithColoredBody("#FF8800", $"Failed to leave {invite.RoomId}:").WithNewline() + .WithCodeBlock(e.ToString(), "cs") + .Build() + ); } - // fall back to room ID - return invite.RoomId; + Invites.Remove(invite); } } \ No newline at end of file diff --git a/MatrixAntiDmSpam/InviteStore.cs b/MatrixAntiDmSpam/InviteStore.cs deleted file mode 100644
index d6e8d49..0000000 --- a/MatrixAntiDmSpam/InviteStore.cs +++ /dev/null
@@ -1,15 +0,0 @@ -using LibMatrix.Utilities.Bot.Services; - -namespace MatrixAntiDmSpam; - -public class InviteStore { - - - public async Task AddInviteAsync(InviteHandlerHostedService.InviteEventArgs invite) { - foreach (var handler in OnInviteReceived) { - await handler(invite); - } - } - - public List<Func<InviteHandlerHostedService.InviteEventArgs, Task>> OnInviteReceived { get; set; } = []; -} \ No newline at end of file diff --git a/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj b/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj
index 4d3c459..703a461 100644 --- a/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj +++ b/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj
@@ -8,8 +8,8 @@ <PropertyGroup Condition="'$(Configuration)' == 'Release'"> <Optimize>true</Optimize> - <RunAOTCompilation>true</RunAOTCompilation> - <PublishTrimmed>true</PublishTrimmed> +<!-- <RunAOTCompilation>true</RunAOTCompilation>--> +<!-- <PublishTrimmed>true</PublishTrimmed>--> <PublishSingleFile>true</PublishSingleFile> <ApplicationIcon>MatrixAntiDmSpam.ico</ApplicationIcon> </PropertyGroup> diff --git a/MatrixAntiDmSpam/PolicyExecutor.cs b/MatrixAntiDmSpam/PolicyExecutor.cs
index a6ca404..65e7f9f 100644 --- a/MatrixAntiDmSpam/PolicyExecutor.cs +++ b/MatrixAntiDmSpam/PolicyExecutor.cs
@@ -1,112 +1,92 @@ +using System.Diagnostics; +using ArcaneLibs.Attributes; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec.State.Policy; -using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.RoomTypes; -using LibMatrix.Utilities.Bot.Services; +using LibMatrix.Utilities.Bot.Interfaces; namespace MatrixAntiDmSpam; public class PolicyExecutor( ILogger<PolicyExecutor> logger, AntiDmSpamConfiguration config, - InviteStore inviteStore, + RoomInviteHandler roomInviteHandler, PolicyStore policyStore, AuthenticatedHomeserverGeneric homeserver) : IHostedService { - private GenericRoom? _logRoom = string.IsNullOrWhiteSpace(config.LogRoom) ? null : homeserver.GetRoom(config.LogRoom); + private readonly GenericRoom? _logRoom = string.IsNullOrWhiteSpace(config.LogRoom) ? null : homeserver.GetRoom(config.LogRoom); - public async Task StartAsync(CancellationToken cancellationToken) { - inviteStore.OnInviteReceived.Add(CheckPoliciesAgainstInvite); - policyStore.OnPolicyUpdated.Add(CheckPolicyAgainstInvites); + public Task StartAsync(CancellationToken cancellationToken) { + roomInviteHandler.OnInviteReceived.Add(CheckPoliciesAgainstInvite); + policyStore.OnPolicyUpdated.Add(CheckPolicyAgainstOutstandingInvites); + return Task.CompletedTask; } - public async Task StopAsync(CancellationToken cancellationToken) { } - - public async Task CheckPoliciesAgainstInvite(InviteHandlerHostedService.InviteEventArgs invite) { - string roomName = await GetRoomNameAsync(invite); - - var roomPolicy = policyStore.RoomPolicies.Any(x => x.Value.EntityMatches(invite.RoomId)) - ? policyStore.RoomPolicies.First(x => x.Value.EntityMatches(invite.RoomId)).Value - : null; - - if (roomPolicy is not null) { - logger.LogWarning("Rejecting invite to {}, matching room policy {}", invite.RoomId, roomPolicy.ToJson(ignoreNull: true)); - - var message = new MessageBuilder() - .WithColoredBody("#FF0000", cb => cb.WithBody("Rejecting invite to ").WithMention(invite.RoomId, roomName).WithBody(", matching room policy.").WithNewline()) - .WithCollapsibleSection("Policy JSON", cb => cb.WithCodeBlock(roomPolicy.ToJson(ignoreNull: true), "json")) - .Build(); - - if (_logRoom is not null) - await _logRoom.SendMessageEventAsync(message); - - await homeserver.GetRoom(invite.RoomId).LeaveAsync(); - } - - var userPolicy = policyStore.UserPolicies.Any(x => x.Value.EntityMatches(invite.MemberEvent.Sender!)) - ? policyStore.UserPolicies.First(x => x.Value.EntityMatches(invite.MemberEvent.Sender!)).Value - : null; - - if (userPolicy is not null) { - logger.LogWarning("Rejecting invite to {}, matching user policy {}", invite.RoomId, userPolicy.ToJson(ignoreNull: true)); - - var message = new MessageBuilder() - .WithColoredBody("#FF0000", cb => cb.WithBody("Rejecting invite to ").WithMention(invite.RoomId, roomName).WithBody(", matching user policy.").WithNewline()) - .WithCollapsibleSection("Policy JSON", cb => cb.WithCodeBlock(userPolicy.ToJson(ignoreNull: true), "json")) - .Build(); - - if (_logRoom is not null) - await _logRoom.SendMessageEventAsync(message); - - await homeserver.GetRoom(invite.RoomId).LeaveAsync(); - } + public Task StopAsync(CancellationToken cancellationToken) { + return Task.CompletedTask; } - public async Task CheckPolicyAgainstInvites(PolicyRuleEventContent policy) { - Console.WriteLine("CheckPolicyAgainstInvites called!!!!"); - } + private Task CheckPoliciesAgainstInvite(RoomInviteContext invite) { + logger.LogInformation("Checking policies against invite"); + var sw = Stopwatch.StartNew(); - private async Task<string> GetRoomNameAsync(InviteHandlerHostedService.InviteEventArgs invite) { - // try to get room name from invite state - var name = invite.InviteData.InviteState?.Events? - .FirstOrDefault(evt => evt is { Type: RoomNameEventContent.EventId, StateKey: "" })? - .ContentAs<RoomNameEventContent>()?.Name; + // Technically not required, but helps with scaling against millions of policies + Parallel.ForEach(policyStore.AllPolicies.Values, (policy, loopState, idx) => { + if (CheckPolicyAgainstInvite(invite, policy) is not null) { + logger.LogInformation("Found matching policy after {} iterations ({})", idx, sw.Elapsed); + loopState.Break(); + } + }); - if (!string.IsNullOrWhiteSpace(name)) - return name; - - // try to get room alias - var alias = invite.InviteData.InviteState?.Events? - .FirstOrDefault(evt => evt is { Type: RoomCanonicalAliasEventContent.EventId, StateKey: "" })? - .ContentAs<RoomCanonicalAliasEventContent>()?.Alias; + return Task.CompletedTask; + } - if (!string.IsNullOrWhiteSpace(alias)) - return alias; + private async Task CheckPolicyAgainstOutstandingInvites(PolicyRuleEventContent policy) { + var tasks = roomInviteHandler.Invites + .Select(invite => CheckPolicyAgainstInvite(invite, policy)) + .Where(x => x is not null) + .Cast<Task>() // from Task? + .ToList(); - // try get room name via public previews - try { -#pragma warning disable CS0618 // Type or member is obsolete - name = await invite.Homeserver.GetRoom(invite.RoomId).GetNameOrFallbackAsync(); -#pragma warning restore CS0618 // Type or member is obsolete - if (name != invite.RoomId && !string.IsNullOrWhiteSpace(name)) - return name; - } - catch { - //ignored - } + await Task.WhenAll(tasks); + } - // fallback to room alias via public previews - try { - alias = (await invite.Homeserver.GetRoom(invite.RoomId).GetCanonicalAliasAsync())?.Alias; - if (!string.IsNullOrWhiteSpace(alias)) - return alias; - } - catch { - //ignored + private Task? CheckPolicyAgainstInvite(RoomInviteContext invite, PolicyRuleEventContent policy) { + if (policy.Recommendation != "m.ban") return null; + + var policyMatches = false; + switch (policy) { + case UserPolicyRuleEventContent userPolicy: + policyMatches = userPolicy.EntityMatches(invite.MemberEvent.Sender!); + break; + case ServerPolicyRuleEventContent serverPolicy: + policyMatches = serverPolicy.EntityMatches(invite.MemberEvent.Sender!); + break; + case RoomPolicyRuleEventContent roomPolicy: + policyMatches = roomPolicy.EntityMatches(invite.RoomId); + break; + default: + if (_logRoom is not null) + _ = _logRoom.SendMessageEventAsync(new MessageBuilder().WithColoredBody("#FF0000", "Unknown policy type " + policy.GetType().FullName).Build()); + break; } - // fall back to room ID - return invite.RoomId; + if (!policyMatches) return null; + logger.LogWarning("Rejecting invite to {}, matching {} {}", invite.RoomId, policy.GetType().GetFriendlyName(), policy.ToJson(ignoreNull: true)); + + return Task.Run(async () => { + if (_logRoom is not null) { + string roomName = await invite.TryGetRoomNameAsync(); + + await roomInviteHandler.RejectInvite(invite, new MessageBuilder() + .WithColoredBody("#FF0000", + cb => cb.WithBody("Rejecting invite to ").WithMention(invite.RoomId, roomName) + .WithBody($", matching {policy.GetType().GetFriendlyName().ToLowerInvariant()}.") + .WithNewline()) + .WithCollapsibleSection("Policy JSON", cb => cb.WithCodeBlock(policy.ToJson(ignoreNull: true), "json")) + ); + } + }); } } \ No newline at end of file diff --git a/MatrixAntiDmSpam/PolicyListFetcher.cs b/MatrixAntiDmSpam/PolicyListFetcher.cs
index c98be08..723550f 100644 --- a/MatrixAntiDmSpam/PolicyListFetcher.cs +++ b/MatrixAntiDmSpam/PolicyListFetcher.cs
@@ -1,4 +1,5 @@ -using System.Diagnostics; +using LibMatrix.Filters; +using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.RoomTypes; @@ -6,14 +7,26 @@ namespace MatrixAntiDmSpam; public class PolicyListFetcher(ILogger<PolicyListFetcher> logger, AntiDmSpamConfiguration config, AuthenticatedHomeserverGeneric homeserver, PolicyStore policyStore) : IHostedService { + private CancellationTokenSource _cts = new(); + public async Task StartAsync(CancellationToken cancellationToken) { + // _ = Enumerable.Range(0, 10_000_000).Select(x => { + // policyStore.AllPolicies.Add(Guid.NewGuid().ToString(), new UserPolicyRuleEventContent() { + // Entity = Guid.NewGuid().ToString() + x, + // Reason = "meow " + x, + // Recommendation = "m.ban" + // }); + // return 0; + // }).ToList(); + logger.LogInformation("Starting policy list fetcher"); await EnsurePolicyListsJoined(); - await LoadPolicyLists(); + _ = SyncPolicyLists(); } public async Task StopAsync(CancellationToken cancellationToken) { logger.LogInformation("Stopping policy list fetcher"); + await _cts.CancelAsync(); } private async Task EnsurePolicyListsJoined() { @@ -21,23 +34,56 @@ public class PolicyListFetcher(ILogger<PolicyListFetcher> logger, AntiDmSpamConf var expectedPolicyRooms = config.PolicyLists; var missingRooms = expectedPolicyRooms.Where(room => !joinedRooms.Any(r => r.RoomId == room.RoomId)).ToList(); - foreach (var room in missingRooms) { + await Task.WhenAll(missingRooms.Select(async room => { logger.LogInformation("Joining policy list room {}", room.RoomId); await homeserver.GetRoom(room.RoomId).JoinAsync(room.Vias); - } + }).ToList()); } - private async Task LoadPolicyLists() { - foreach (var room in config.PolicyLists) { - var sw = Stopwatch.StartNew(); - var count = await LoadPolicyList(homeserver.GetRoom(room.RoomId).AsPolicyRoom()); - logger.LogInformation("Loaded policy list {} ({}) in {}, with {} policies", room.Name, room.RoomId, sw.Elapsed, count); - } - } + private async Task SyncPolicyLists() { + var syncHelper = new SyncHelper(homeserver, logger) { + Timeout = 30_000, + MinimumDelay = TimeSpan.FromSeconds(3), + FilterId = (await homeserver.UploadFilterAsync(new SyncFilter() { + AccountData = SyncFilter.EventFilter.Empty, + Presence = SyncFilter.EventFilter.Empty, + Room = new SyncFilter.RoomFilter { + AccountData = SyncFilter.RoomFilter.StateFilter.Empty, + Ephemeral = SyncFilter.RoomFilter.StateFilter.Empty, + State = new SyncFilter.RoomFilter.StateFilter(types: PolicyRoom.SpecPolicyEventTypes.ToList()), + Timeline = new SyncFilter.RoomFilter.StateFilter(types: PolicyRoom.SpecPolicyEventTypes.ToList()), + Rooms = config.PolicyLists.Select(x => x.RoomId).ToList(), + IncludeLeave = false + } + })).FilterId + }; + + await foreach (var syncResponse in syncHelper.EnumerateSyncAsync(_cts.Token)) { + if (_cts.IsCancellationRequested) return; - private async Task<int> LoadPolicyList(PolicyRoom room) { - var policies = room.GetPoliciesAsync().ToBlockingEnumerable().ToList(); - await policyStore.AddPolicies(policies); - return policies.Count(); + if (syncResponse is { Rooms.Join.Count: > 0 }) { + foreach (var (roomId, data) in syncResponse.Rooms.Join) { + if (!config.PolicyLists.Any(x => x.RoomId == roomId)) continue; + + if (data.State?.Events is null) + data.State = new() { Events = [] }; + + if (data.Timeline is { Events.Count: > 0 }) { + data.State.Events.AddRange(data.Timeline.Events); + data.Timeline = null; + } + } + + var newPolicies = syncResponse.Rooms!.Join!.SelectMany(x => x.Value.State!.Events!) + .Where(x => PolicyRoom.SpecPolicyEventTypes.Contains(x.Type)) + .ToList(); + + logger.LogWarning("Received non-empty sync response with {}/{}/{} rooms, resulting in {} policies", syncResponse.Rooms?.Join?.Count, + syncResponse.Rooms?.Invite?.Count, + syncResponse.Rooms?.Leave?.Count, newPolicies.Count); + + await policyStore.AddPoliciesAsync(newPolicies); + } + } } } \ No newline at end of file diff --git a/MatrixAntiDmSpam/PolicyStore.cs b/MatrixAntiDmSpam/PolicyStore.cs
index f2ddd36..863bc92 100644 --- a/MatrixAntiDmSpam/PolicyStore.cs +++ b/MatrixAntiDmSpam/PolicyStore.cs
@@ -1,33 +1,24 @@ using LibMatrix; -using System.Collections; using LibMatrix.EventTypes.Spec.State.Policy; namespace MatrixAntiDmSpam; public class PolicyStore { - public Dictionary<string, UserPolicyRuleEventContent> UserPolicies { get; } = []; - public Dictionary<string, ServerPolicyRuleEventContent> ServerPolicies { get; } = []; - public Dictionary<string, RoomPolicyRuleEventContent> RoomPolicies { get; } = []; + public Dictionary<string, PolicyRuleEventContent> AllPolicies { get; } = []; public List<Func<PolicyRuleEventContent, Task>> OnPolicyUpdated { get; } = []; - public async Task AddPolicies(IEnumerable<StateEventResponse> events) => events.ToList().Select(AddPolicy).ToList(); + public Task AddPoliciesAsync(IEnumerable<StateEventResponse> events) => Task.WhenAll(events.Select(AddPolicyAsync).ToList()); - public async Task AddPolicy(StateEventResponse evt) { + public async Task AddPolicyAsync(StateEventResponse evt) { var eventKey = $"{evt.RoomId}:{evt.Type}:{evt.StateKey}"; - switch (evt.TypedContent) { - case UserPolicyRuleEventContent userPolicy: - UserPolicies[eventKey] = userPolicy; - break; - case ServerPolicyRuleEventContent serverPolicy: - ServerPolicies[eventKey] = serverPolicy; - break; - case RoomPolicyRuleEventContent roomPolicy: - RoomPolicies[eventKey] = roomPolicy; - break; - } + if (evt.TypedContent is PolicyRuleEventContent policy) { + if (policy.Recommendation == "m.ban") + AllPolicies[eventKey] = policy; + else AllPolicies.Remove(eventKey); - foreach (var callback in OnPolicyUpdated) { - await callback((evt.TypedContent as PolicyRuleEventContent)!); + foreach (var callback in OnPolicyUpdated) { + await callback(policy); + } } } } \ No newline at end of file diff --git a/MatrixAntiDmSpam/Program.cs b/MatrixAntiDmSpam/Program.cs
index e81cd58..e3eb037 100644 --- a/MatrixAntiDmSpam/Program.cs +++ b/MatrixAntiDmSpam/Program.cs
@@ -8,12 +8,11 @@ var builder = Host.CreateApplicationBuilder(args); builder.Services.AddSingleton<AntiDmSpamConfiguration>(); builder.Services.AddRoryLibMatrixServices() .AddMatrixBot() - .WithInviteHandler<InviteHandler>(); + .WithInviteHandler<RoomInviteHandler>(); builder.Services.AddHostedService<PolicyListFetcher>(); builder.Services.AddHostedService<PolicyExecutor>(); -builder.Services.AddSingleton<InviteStore>(); builder.Services.AddSingleton<PolicyStore>(); MatrixHttpClient.LogRequests = false; diff --git a/MatrixAntiDmSpam/Properties/launchSettings.json b/MatrixAntiDmSpam/Properties/launchSettings.json
index 383a8e2..f2c4644 100644 --- a/MatrixAntiDmSpam/Properties/launchSettings.json +++ b/MatrixAntiDmSpam/Properties/launchSettings.json
@@ -7,6 +7,13 @@ "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" } + }, + "MatrixInviteLogger Local": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Local" + } } } }