using System.Diagnostics; using System.Runtime.CompilerServices; using ArcaneLibs.Attributes; using ArcaneLibs.Extensions; using LibMatrix; using LibMatrix.EventTypes.Spec.State.Policy; using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.RoomTypes; using LibMatrix.Utilities.Bot.Interfaces; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MatrixAntiDmSpam.Core; public class InviteManager( ILogger logger, AntiDmSpamConfiguration config, RoomInviteHandler roomInviteHandler, PolicyStore policyStore, AuthenticatedHomeserverGeneric homeserver) : IHostedService { private readonly GenericRoom? _logRoom = string.IsNullOrWhiteSpace(config.LogRoom) ? null : homeserver.GetRoom(config.LogRoom); public List> OnInviteRejected { get; } = []; public List> OnBeforeInviteRejected { get; } = []; public async Task StartAsync(CancellationToken cancellationToken) { roomInviteHandler.OnInviteReceived.Add(CheckPoliciesAgainstInvite); policyStore.OnPolicyAdded.Add(CheckPolicyAgainstOutstandingInvites); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; private Task CheckPoliciesAgainstInvite(RoomInviteContext invite) { logger.LogInformation("Checking policies against invite"); var sw = Stopwatch.StartNew(); // 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(); } }); return Task.CompletedTask; } private async Task CheckPolicyAgainstOutstandingInvites(StateEventResponse newEvent) { var tasks = roomInviteHandler.Invites .Select(invite => CheckPolicyAgainstInvite(invite, newEvent)) .Where(x => x is not null) .Cast() // from Task? .ToList(); await Task.WhenAll(tasks); } private Task? CheckPolicyAgainstInvite(RoomInviteContext invite, StateEventResponse policyEvent) { var policy = policyEvent.TypedContent as PolicyRuleEventContent ?? throw new InvalidOperationException("Policy is null"); if (policy.GetNormalizedRecommendation() is not ("m.ban" or "m.takedown")) 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; } if (!policyMatches) { logger.LogTrace("[{}] Policy {} does not match invite to {} by {}: {}", homeserver.WhoAmI.UserId, policy.GetType().GetFriendlyName(), invite.RoomId, invite.MemberEvent.Sender, policy.ToJson(ignoreNull: true, indent: false)); return null; } return LogAndRejectInvite(invite, policyEvent, policy); } private async Task LogAndRejectInvite(RoomInviteContext invite, StateEventResponse policyEvent, PolicyRuleEventContent policy) { var policyRoom = config.PolicyLists.First(x => x.RoomId == policyEvent.RoomId); logger.LogWarning("[{}] Rejecting invite to {}, matching {} in {}: {}", homeserver.WhoAmI.UserId, invite.RoomId, policy.GetType().GetFriendlyName(), policyRoom.Name, policy.ToJson(ignoreNull: true)); foreach (var callback in OnBeforeInviteRejected) { await callback(invite, policyEvent); } if (_logRoom is not null) { var roomName = await invite.TryGetRoomNameAsync(); var logMessage = new MessageBuilder() .WithColoredBody("#FF0000", cb => cb.WithBody("Rejecting invite to ").WithMention(invite.RoomId, roomName) .WithBody($", matching {policy.GetType().GetFriendlyName().ToLowerInvariant()} in {policyRoom.Name}.") .WithNewline()) .WithCollapsibleSection("Policy JSON", cb => cb.WithCodeBlock(policy.ToJson(ignoreNull: true), "json")) .Build(); await _logRoom.SendMessageEventAsync(logMessage); } await roomInviteHandler.RejectInvite(invite); foreach (var callback in OnInviteRejected) { await callback(invite, policyEvent); } } }