diff --git a/MatrixAntiDmSpam/AntiDmSpamConfiguration.cs b/MatrixAntiDmSpam/AntiDmSpamConfiguration.cs
new file mode 100644
index 0000000..0e10a91
--- /dev/null
+++ b/MatrixAntiDmSpam/AntiDmSpamConfiguration.cs
@@ -0,0 +1,14 @@
+namespace MatrixAntiDmSpam;
+
+public class AntiDmSpamConfiguration {
+ public AntiDmSpamConfiguration(IConfiguration config) => config.GetRequiredSection("AntiDmSpam").Bind(this);
+ public string? LogRoom { get; set; }
+ public bool LogInviteDataAsFile { get; set; }
+
+ public List<PolicyRoomReference> PolicyLists { get; set; }
+
+ public class PolicyRoomReference {
+ public string RoomId { get; set; }
+ public List<string> Vias { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/MatrixAntiDmSpam/InviteHandler.cs b/MatrixAntiDmSpam/InviteHandler.cs
new file mode 100644
index 0000000..cfa04dc
--- /dev/null
+++ b/MatrixAntiDmSpam/InviteHandler.cs
@@ -0,0 +1,110 @@
+using System.Text.Json;
+using ArcaneLibs;
+using ArcaneLibs.Extensions;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Helpers;
+using LibMatrix.Utilities.Bot.Services;
+
+namespace MatrixAntiDmSpam;
+
+public class InviteHandler(ILogger<InviteHandler> logger, AntiDmSpamConfiguration config) : InviteHandlerHostedService.IInviteHandler {
+ public async Task HandleInviteAsync(InviteHandlerHostedService.InviteEventArgs invite) {
+ logger.LogInformation("Received invite to room {}", invite.RoomId);
+ await LogInvite(invite);
+ }
+
+ 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);
+
+ logger.LogInformation("Inviter: {}, Room: {}", inviterName, roomName);
+
+ var message = new MessageBuilder()
+ .WithBody("Received invite to ").WithMention(invite.RoomId, roomName).WithBody(" from ").WithMention(invite.MemberEvent.Sender!, inviterName)
+ .Build();
+
+ // 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!;
+
+ 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;
+
+ await logRoom.SendMessageEventAsync(message);
+
+ if (config.LogInviteDataAsFile) {
+ await logRoom.SendMessageEventAsync(new() {
+ MessageType = "m.file",
+ Body = invite.RoomId + ".json",
+ FileName = invite.RoomId + ".json",
+ Url = inviteDataFileUri,
+ 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 {
+ name = await invite.Homeserver.GetRoom(invite.RoomId).GetNameOrFallbackAsync();
+ if (name != invite.RoomId && !string.IsNullOrWhiteSpace(name))
+ return name;
+ }
+ catch {
+ //ignored
+ }
+
+ // 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
+ }
+
+ // fall back to room ID
+ return invite.RoomId;
+ }
+}
\ No newline at end of file
diff --git a/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj b/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj
new file mode 100644
index 0000000..5a322ce
--- /dev/null
+++ b/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk.Worker">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UserSecretsId>dotnet-MatrixInviteLogger-87d8c346-8c07-42f9-8bfb-f2a714bbd663</UserSecretsId>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/MatrixAntiDmSpam/PolicyListFetcher.cs b/MatrixAntiDmSpam/PolicyListFetcher.cs
new file mode 100644
index 0000000..0119527
--- /dev/null
+++ b/MatrixAntiDmSpam/PolicyListFetcher.cs
@@ -0,0 +1,27 @@
+using LibMatrix.Homeservers;
+
+namespace MatrixAntiDmSpam;
+
+public class PolicyListFetcher(ILogger<PolicyListFetcher> logger, AntiDmSpamConfiguration config, AuthenticatedHomeserverGeneric homeserver) : IHostedService {
+
+
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ logger.LogInformation("Starting policy list fetcher");
+ await EnsurePolicyListsJoined();
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) {
+ logger.LogInformation("Stopping policy list fetcher");
+ }
+
+ 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();
+
+ foreach (var room in missingRooms) {
+ logger.LogInformation("Joining policy list room {}", room.RoomId);
+ await homeserver.GetRoom(room.RoomId).JoinAsync(room.Vias);
+ }
+ }
+}
\ No newline at end of file
diff --git a/MatrixAntiDmSpam/Program.cs b/MatrixAntiDmSpam/Program.cs
new file mode 100644
index 0000000..cbf1dc9
--- /dev/null
+++ b/MatrixAntiDmSpam/Program.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel;
+using LibMatrix.Services;
+using LibMatrix.Utilities.Bot;
+using MatrixAntiDmSpam;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+builder.Services.AddSingleton<AntiDmSpamConfiguration>();
+builder.Services.AddRoryLibMatrixServices()
+ .AddMatrixBot()
+ .WithInviteHandler<InviteHandler>();
+
+builder.Services.AddHostedService<PolicyListFetcher>();
+
+var host = builder.Build();
+host.Run();
\ No newline at end of file
diff --git a/MatrixAntiDmSpam/Properties/launchSettings.json b/MatrixAntiDmSpam/Properties/launchSettings.json
new file mode 100644
index 0000000..383a8e2
--- /dev/null
+++ b/MatrixAntiDmSpam/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "MatrixInviteLogger": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/MatrixAntiDmSpam/appsettings.Development.json b/MatrixAntiDmSpam/appsettings.Development.json
new file mode 100644
index 0000000..45717aa
--- /dev/null
+++ b/MatrixAntiDmSpam/appsettings.Development.json
@@ -0,0 +1,16 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "LibMatrixBot": {
+ "Homeserver": "rory.gay",
+ "AccessTokenPath": "/home/Rory/matrix_access_token"
+ },
+ "AntiDmSpam": {
+ "LogRoom": "!GrLSwdAkdrvfMrRYKR:rory.gay",
+ "LogInviteDataAsFile": true
+ }
+}
diff --git a/MatrixAntiDmSpam/appsettings.json b/MatrixAntiDmSpam/appsettings.json
new file mode 100644
index 0000000..b2dcdb6
--- /dev/null
+++ b/MatrixAntiDmSpam/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
|