diff --git a/MiniUtils.Core/AntiDmSpamConfiguration.cs b/MiniUtils.Core/AntiDmSpamConfiguration.cs
new file mode 100644
index 0000000..9db6b8c
--- /dev/null
+++ b/MiniUtils.Core/AntiDmSpamConfiguration.cs
@@ -0,0 +1,20 @@
+using Microsoft.Extensions.Configuration;
+
+namespace MiniUtils.Core;
+
+public class AntiDmSpamConfiguration {
+ public AntiDmSpamConfiguration(IConfiguration config) => config.GetRequiredSection("AntiDmSpam").Bind(this);
+ public string? LogRoom { get; set; }
+ public bool LogInviteDataAsFile { get; set; } = true;
+ public bool IgnoreBannedUsers { get; set; } = false;
+ public bool ReportBlockedInvites { get; set; } = false;
+ public TimeSpan MinimumSyncTime { get; set; } = TimeSpan.Zero;
+
+ public required List<PolicyRoomReference> PolicyLists { get; set; }
+
+ public class PolicyRoomReference {
+ public required string Name { get; set; }
+ public required string RoomId { get; set; }
+ public required List<string> Vias { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/MiniUtils.Core/Classes/MADSIgnoreMetadataContent.cs b/MiniUtils.Core/Classes/MADSIgnoreMetadataContent.cs
new file mode 100644
index 0000000..10eddd0
--- /dev/null
+++ b/MiniUtils.Core/Classes/MADSIgnoreMetadataContent.cs
@@ -0,0 +1,25 @@
+using System.Text.Json.Serialization;
+
+namespace MiniUtils.Core.Classes;
+
+public class MadsIgnoreMetadataContent {
+ public const string EventId = "gay.rory.MatrixAntiDmSpam.ignore_metadata";
+
+ // Whether the ignore entry already existed, if true, do not remove when no matching policies exist
+ [JsonPropertyName("was_user_added")]
+ public required bool WasUserAdded { get; set; }
+
+ [JsonPropertyName("policies")]
+ public required List<PolicyEventReference> Policies { get; set; }
+
+ public class PolicyEventReference {
+ [JsonPropertyName("room_id")]
+ public required string RoomId { get; set; }
+
+ [JsonPropertyName("type")]
+ public required string Type { get; set; }
+
+ [JsonPropertyName("state_key")]
+ public required string StateKey { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/MiniUtils.Core/MiniUtils.Core.csproj b/MiniUtils.Core/MiniUtils.Core.csproj
new file mode 100644
index 0000000..6e9363f
--- /dev/null
+++ b/MiniUtils.Core/MiniUtils.Core.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ <LangVersion>preview</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-preview.3.25171.5" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
+ <ProjectReference Include="..\LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/MiniUtils.Core/PolicyExecutor.cs b/MiniUtils.Core/PolicyExecutor.cs
new file mode 100644
index 0000000..80bbb99
--- /dev/null
+++ b/MiniUtils.Core/PolicyExecutor.cs
@@ -0,0 +1,230 @@
+using System.Diagnostics;
+using System.Text.Json.Nodes;
+using ArcaneLibs.Attributes;
+using ArcaneLibs.Extensions;
+using LibMatrix;
+using LibMatrix.EventTypes.Spec;
+using LibMatrix.EventTypes.Spec.State.Policy;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using LibMatrix.RoomTypes;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MiniUtils.Core.Classes;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MiniUtils.Core;
+
+public class PolicyExecutor(
+ ILogger<PolicyExecutor> logger,
+ AntiDmSpamConfiguration config,
+ RoomInviteHandler roomInviteHandler,
+ PolicyStore policyStore,
+ AuthenticatedHomeserverGeneric homeserver) : IHostedService {
+ private readonly GenericRoom? _logRoom = string.IsNullOrWhiteSpace(config.LogRoom) ? null : homeserver.GetRoom(config.LogRoom);
+
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ roomInviteHandler.OnInviteReceived.Add(CheckPoliciesAgainstInvite);
+ policyStore.OnPolicyAdded.Add(CheckPolicyAgainstOutstandingInvites);
+ if (config.IgnoreBannedUsers) {
+ await CleanupInvalidIgnoreListEntries();
+ policyStore.OnPoliciesChanged.Add(UpdateIgnoreList);
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+#region Feature: Manage ignore list
+
+ private async Task UpdateIgnoreList((
+ List<StateEventResponse> NewPolicies,
+ List<(StateEventResponse Old, StateEventResponse New)> UpdatedPolicies,
+ List<(StateEventResponse Old, StateEventResponse New)> RemovedPolicies) updates
+ ) {
+ var ignoreListContent = await FilterInvalidIgnoreListEntries();
+
+ foreach (var newEvent in updates.NewPolicies) {
+ var content = newEvent.TypedContent as PolicyRuleEventContent;
+ if (content.Entity is null || content.IsGlobRule()) continue;
+ if (content.GetNormalizedRecommendation() != "m.ban") continue;
+
+ var policyEventReference = new MadsIgnoreMetadataContent.PolicyEventReference() {
+ Type = newEvent.Type,
+ RoomId = newEvent.RoomId ?? throw new InvalidOperationException("RoomId is null"),
+ StateKey = newEvent.StateKey!
+ };
+
+ if (ignoreListContent.IgnoredUsers.TryGetValue(content.Entity, out var existingRule)) {
+ if (existingRule.AdditionalData?.ContainsKey(MadsIgnoreMetadataContent.EventId) ?? false) {
+ var existingMetadata = existingRule.GetAdditionalData<MadsIgnoreMetadataContent>(MadsIgnoreMetadataContent.EventId);
+ existingMetadata.Policies.Add(policyEventReference);
+ }
+ else {
+ existingRule.AdditionalData ??= new();
+ existingRule.AdditionalData.Add(MadsIgnoreMetadataContent.EventId, new MadsIgnoreMetadataContent {
+ WasUserAdded = true,
+ Policies = [policyEventReference]
+ });
+ }
+ }
+ else {
+ ignoreListContent.IgnoredUsers[content.Entity] = new() {
+ AdditionalData = new() {
+ [MadsIgnoreMetadataContent.EventId] = new MadsIgnoreMetadataContent {
+ WasUserAdded = false,
+ Policies = [policyEventReference]
+ }
+ }
+ };
+ }
+ }
+
+ foreach (var (previousEvent, newEvent) in updates.RemovedPolicies) {
+ if (previousEvent.Type != UserPolicyRuleEventContent.EventId) continue;
+ var previousContent = previousEvent.ContentAs<UserPolicyRuleEventContent>();
+
+ if (previousContent.Entity is null || previousContent.IsGlobRule()) continue;
+ if (previousContent.GetNormalizedRecommendation() != "m.ban") continue;
+
+ var ignoreList = await homeserver.GetIgnoredUserListAsync();
+ if (ignoreList.IgnoredUsers.TryGetValue(previousContent.Entity, out var existingRule)) {
+ if (existingRule.AdditionalData?.ContainsKey(MadsIgnoreMetadataContent.EventId) ?? false) {
+ var existingMetadata = existingRule.GetAdditionalData<MadsIgnoreMetadataContent>(MadsIgnoreMetadataContent.EventId);
+ existingMetadata.Policies.RemoveAll(x => x.Type == previousEvent.Type && x.RoomId == previousEvent.RoomId && x.StateKey == previousEvent.StateKey);
+ if (!existingMetadata.WasUserAdded)
+ ignoreList.IgnoredUsers.Remove(previousContent.Entity);
+ }
+ }
+ }
+
+ await homeserver.SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoreListContent);
+ }
+
+ private async Task<IgnoredUserListEventContent> FilterInvalidIgnoreListEntries() {
+ var ignoreList = await homeserver.GetAccountDataOrNullAsync<IgnoredUserListEventContent>(IgnoredUserListEventContent.EventId);
+ if (ignoreList != null) {
+ ignoreList.IgnoredUsers.RemoveAll((id, ignoredUserData) => {
+ if (ignoredUserData.AdditionalData is null) return false;
+ if (!ignoredUserData.AdditionalData.ContainsKey(MadsIgnoreMetadataContent.EventId)) return false;
+ var metadata = ignoredUserData.GetAdditionalData<JsonObject>(MadsIgnoreMetadataContent.EventId)!;
+
+ if (metadata.ContainsKey("policies")) {
+ var policies = metadata["policies"]!.AsArray();
+
+ bool IsPolicyEntryValid(JsonNode? p) =>
+ p!["room_id"]?.GetValue<string>() != null && p["type"]?.GetValue<string>() != null && p["state_key"]?.GetValue<string>() != null;
+
+ if (policies.Any(x => !IsPolicyEntryValid(x))) {
+ logger.LogWarning("Found invalid policy reference in ignore list, removing! {policy}",
+ policies.Where(x => !IsPolicyEntryValid(x)).Select(x => x.ToJson(ignoreNull: true)));
+ metadata["policies"] = new JsonArray(policies.Where(IsPolicyEntryValid).ToArray());
+ }
+ }
+
+ return metadata["was_user_added"]?.GetValue<bool>() is null or false;
+ });
+ }
+
+ return ignoreList;
+ }
+
+ private async Task CleanupInvalidIgnoreListEntries() {
+ var ignoreList = await FilterInvalidIgnoreListEntries();
+ ignoreList.IgnoredUsers.RemoveAll((id, _) => !(id.StartsWith('@') && id.Contains(':')));
+ List<string> idsToRemove = [];
+ foreach (var (id, ignoredUserData) in ignoreList.IgnoredUsers) {
+ if (ignoredUserData.AdditionalData is null) continue;
+ if (!ignoredUserData.AdditionalData.ContainsKey(MadsIgnoreMetadataContent.EventId)) continue;
+ try {
+ var metadata = ignoredUserData.GetAdditionalData<MadsIgnoreMetadataContent>(MadsIgnoreMetadataContent.EventId)!;
+
+ if (metadata.Policies.Count == 0 && !metadata.WasUserAdded) {
+ idsToRemove.Add(id);
+ }
+ }
+ catch (Exception e) {
+ logger.LogError(e, "Failed to parse ignore list entry for {}", id);
+ }
+ }
+
+ foreach (var id in idsToRemove) {
+ ignoreList.IgnoredUsers.Remove(id);
+ }
+ await homeserver.SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoreList);
+ }
+
+#endregion
+
+#region Feature: Report blocked invites
+
+#endregion
+
+#region Feature: Reject invites
+
+ 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<Task>() // 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.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;
+ }
+
+ if (!policyMatches) return null;
+ logger.LogWarning("[{}] Rejecting invite to {}, matching {} {}", homeserver.WhoAmI.UserId, 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"))
+ );
+ }
+ });
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/MiniUtils.Core/PolicyListFetcher.cs b/MiniUtils.Core/PolicyListFetcher.cs
new file mode 100644
index 0000000..b177112
--- /dev/null
+++ b/MiniUtils.Core/PolicyListFetcher.cs
@@ -0,0 +1,86 @@
+using LibMatrix.Filters;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using LibMatrix.RoomTypes;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MiniUtils.Core;
+
+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();
+ _ = SyncPolicyLists().ContinueWith(x => {
+ if (x.IsFaulted) {
+ logger.LogError(x.Exception, "Error in policy list fetcher");
+ }
+ }, TaskContinuationOptions.OnlyOnFaulted);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) {
+ logger.LogInformation("Stopping policy list fetcher");
+ await _cts.CancelAsync();
+ }
+
+ private async Task EnsurePolicyListsJoined() {
+ var joinedRooms = await homeserver.GetJoinedRooms();
+ var expectedPolicyRooms = config.PolicyLists;
+ var missingRooms = expectedPolicyRooms.Where(room => !joinedRooms.Any(r => r.RoomId == room.RoomId)).ToList();
+
+ 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 SyncPolicyLists() {
+ var syncHelper = new SyncHelper(homeserver, logger) {
+ Timeout = 30_000,
+ MinimumDelay = config.MinimumSyncTime,
+ 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,
+ UseMsc4222StateAfter = true
+ };
+
+ await foreach (var syncResponse in syncHelper.EnumerateSyncAsync(_cts.Token)) {
+ if (_cts.IsCancellationRequested) return;
+
+ if (syncResponse is { Rooms.Join.Count: > 0 }) {
+ var newPolicies = syncResponse.Rooms.Join
+ .Where(x => config.PolicyLists.Any(y => y.RoomId == x.Key))
+ .SelectMany(x => x.Value.StateAfter?.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/MiniUtils.Core/PolicyStore.cs b/MiniUtils.Core/PolicyStore.cs
new file mode 100644
index 0000000..f2aa0fc
--- /dev/null
+++ b/MiniUtils.Core/PolicyStore.cs
@@ -0,0 +1,104 @@
+using System.Diagnostics;
+using LibMatrix;
+using LibMatrix.EventTypes.Spec.State.Policy;
+using Microsoft.Extensions.Logging;
+
+namespace MiniUtils.Core;
+
+public class PolicyStore(ILogger<PolicyStore> logger) {
+ public Dictionary<string, StateEventResponse> AllPolicies { get; } = [];
+
+#region Single policy events
+
+ /// <summary>
+ /// Fired when any policy event is received
+ /// </summary>
+ public List<Func<StateEventResponse, Task>> OnPolicyReceived { get; } = [];
+
+ /// <summary>
+ /// Fired when a new policy is added
+ /// </summary>
+ public List<Func<StateEventResponse, Task>> OnPolicyAdded { get; } = [];
+
+ /// <summary>
+ /// Fired when a policy is updated, without being removed.
+ /// </summary>
+ public List<Func<StateEventResponse, StateEventResponse, Task>> OnPolicyUpdated { get; } = [];
+
+ /// <summary>
+ /// Fired when a policy is removed.
+ /// </summary>
+ public List<Func<StateEventResponse, StateEventResponse, Task>> OnPolicyRemoved { get; } = [];
+
+#endregion
+
+#region Bulk policy events
+
+ /// <summary>
+ /// Fired when any policy event is received
+ /// </summary>
+ public List<Func<(List<StateEventResponse> NewPolicies,
+ List<(StateEventResponse Old, StateEventResponse New)> UpdatedPolicies,
+ List<(StateEventResponse Old, StateEventResponse New)> RemovedPolicies), Task>> OnPoliciesChanged { get; } = [];
+
+#endregion
+
+ public async Task AddPoliciesAsync(IEnumerable<StateEventResponse> events) {
+ var policyEvents = events
+ .Where(evt => evt.TypedContent is PolicyRuleEventContent)
+ .ToList();
+ List<StateEventResponse> newPolicies = new();
+ List<(StateEventResponse Old, StateEventResponse New)> updatedPolicies = new();
+ List<(StateEventResponse Old, StateEventResponse New)> removedPolicies = new();
+
+ var sw = Stopwatch.StartNew();
+ try {
+ foreach (var evt in policyEvents) {
+ var eventKey = $"{evt.RoomId}:{evt.Type}:{evt.StateKey}";
+ if (evt.TypedContent is not PolicyRuleEventContent policy) continue;
+ if (policy.GetNormalizedRecommendation() is "m.ban") {
+ if (AllPolicies.TryGetValue(eventKey, out var oldContent))
+ updatedPolicies.Add((oldContent, evt));
+ else
+ newPolicies.Add(evt);
+ AllPolicies[eventKey] = evt;
+ }
+ else if (AllPolicies.Remove(eventKey, out var oldContent))
+ removedPolicies.Add((oldContent, evt));
+ }
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ }
+
+ logger.LogInformation("Processed {Count} policies in {Elapsed}", policyEvents.Count, sw.Elapsed);
+
+
+ // execute all the callbacks in parallel, as much as possible...
+ await Task.WhenAll(
+ Task.WhenAll(
+ policyEvents.Select(evt =>
+ Task.WhenAll(OnPolicyReceived.Select(callback => callback(evt)).ToList())
+ )
+ ),
+ Task.WhenAll(
+ newPolicies.Select(evt =>
+ Task.WhenAll(OnPolicyAdded.Select(callback => callback(evt)).ToList())
+ )
+ ),
+ Task.WhenAll(
+ updatedPolicies.Select(evt =>
+ Task.WhenAll(OnPolicyUpdated.Select(callback => callback(evt.Old, evt.New)).ToList())
+ )
+ ),
+ Task.WhenAll(
+ removedPolicies.Select(evt =>
+ Task.WhenAll(OnPolicyRemoved.Select(callback => callback(evt.Old, evt.New)).ToList())
+ )
+ ),
+ Task.WhenAll(OnPoliciesChanged.Select(callback => callback((newPolicies, updatedPolicies, removedPolicies))).ToList())
+ );
+ }
+
+ private async Task AddPolicyAsync(StateEventResponse evt) { }
+}
\ No newline at end of file
diff --git a/MiniUtils.Core/RoomInviteHandler.cs b/MiniUtils.Core/RoomInviteHandler.cs
new file mode 100644
index 0000000..4597522
--- /dev/null
+++ b/MiniUtils.Core/RoomInviteHandler.cs
@@ -0,0 +1,92 @@
+using System.Text.Json;
+using ArcaneLibs;
+using ArcaneLibs.Extensions;
+using LibMatrix.Helpers;
+using LibMatrix.RoomTypes;
+using LibMatrix.Utilities.Bot.Interfaces;
+using Microsoft.Extensions.Logging;
+
+namespace MiniUtils.Core;
+
+public class RoomInviteHandler(ILogger<RoomInviteHandler> logger, AntiDmSpamConfiguration config) : IRoomInviteHandler {
+ public List<RoomInviteContext> Invites { get; } = [];
+
+ public List<Func<RoomInviteContext, Task>> OnInviteReceived { get; set; } = [];
+
+ private GenericRoom? LogRoom { get; set; }
+
+ public async Task HandleInviteAsync(RoomInviteContext invite) {
+ if (!string.IsNullOrWhiteSpace(config.LogRoom))
+ LogRoom = invite.Homeserver.GetRoom(config.LogRoom);
+
+ 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 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);
+
+ 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() {
+ MessageType = "m.file",
+ Body = invite.RoomId + ".json",
+ FileName = invite.RoomId + ".json",
+ Url = await inviteDataFileUriTask,
+ FileInfo = new() { Size = inviteData.Length, MimeType = "application/json" }
+ });
+ }
+ }
+
+ public async Task RejectInvite(RoomInviteContext invite, MessageBuilder reason) {
+ if (LogRoom is not null)
+ _ = LogRoom.SendMessageEventAsync(reason.Build());
+
+ try {
+ await invite.Homeserver.GetRoom(invite.RoomId).LeaveAsync();
+ }
+ 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()
+ );
+ }
+
+ Invites.Remove(invite);
+ }
+}
\ No newline at end of file
|