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
|