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"
+ }
}
}
}
|