about summary refs log tree commit diff
path: root/MiniUtils.Core
diff options
context:
space:
mode:
Diffstat (limited to 'MiniUtils.Core')
-rw-r--r--MiniUtils.Core/AntiDmSpamConfiguration.cs20
-rw-r--r--MiniUtils.Core/Classes/MADSIgnoreMetadataContent.cs25
-rw-r--r--MiniUtils.Core/MiniUtils.Core.csproj19
-rw-r--r--MiniUtils.Core/PolicyExecutor.cs230
-rw-r--r--MiniUtils.Core/PolicyListFetcher.cs86
-rw-r--r--MiniUtils.Core/PolicyStore.cs104
-rw-r--r--MiniUtils.Core/RoomInviteHandler.cs92
7 files changed, 576 insertions, 0 deletions
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