diff options
author | Rory& <root@rory.gay> | 2024-02-23 13:57:06 +0100 |
---|---|---|
committer | Rory& <root@rory.gay> | 2024-02-23 13:57:06 +0100 |
commit | d0d11db2209a8be65c27e15ca9d8a3b594f1a352 (patch) | |
tree | b42b7de4b09888a1439d0939707ba1331becf626 /MatrixUtils.DmSpaced | |
parent | HS emulator (diff) | |
download | MatrixUtils-d0d11db2209a8be65c27e15ca9d8a3b594f1a352.tar.xz |
Add eons of work because I forgot to push
Diffstat (limited to 'MatrixUtils.DmSpaced')
-rw-r--r-- | MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj | 31 | ||||
-rw-r--r-- | MatrixUtils.DmSpaced/ModerationBot.cs | 298 | ||||
-rw-r--r-- | MatrixUtils.DmSpaced/ModerationBotConfiguration.cs | 10 | ||||
-rw-r--r-- | MatrixUtils.DmSpaced/Program.cs | 40 | ||||
-rw-r--r-- | MatrixUtils.DmSpaced/appsettings.Development.json | 24 | ||||
-rw-r--r-- | MatrixUtils.DmSpaced/appsettings.json | 9 |
6 files changed, 412 insertions, 0 deletions
diff --git a/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj new file mode 100644 index 0000000..4b0f599 --- /dev/null +++ b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj @@ -0,0 +1,31 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <LangVersion>preview</LangVersion> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <PublishAot>false</PublishAot> + <InvariantGlobalization>true</InvariantGlobalization> + <!-- <PublishTrimmed>true</PublishTrimmed>--> + <!-- <PublishReadyToRun>true</PublishReadyToRun>--> + <!-- <PublishSingleFile>true</PublishSingleFile>--> + <!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>--> + <!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>--> + <!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>--> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> + </ItemGroup> + <ItemGroup> + <Content Include="appsettings*.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + </ItemGroup> +</Project> diff --git a/MatrixUtils.DmSpaced/ModerationBot.cs b/MatrixUtils.DmSpaced/ModerationBot.cs new file mode 100644 index 0000000..6e534fc --- /dev/null +++ b/MatrixUtils.DmSpaced/ModerationBot.cs @@ -0,0 +1,298 @@ +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.Policy; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.RoomTypes; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace ModerationBot; + +public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<ModerationBot> logger, ModerationBotConfiguration configuration, PolicyEngine engine) : IHostedService { + private Task _listenerTask; + + // private GenericRoom _policyRoom; + private GenericRoom? _logRoom; + private GenericRoom? _controlRoom; + + /// <summary>Triggered when the application host is ready to start the service.</summary> + /// <param name="cancellationToken">Indicates that the start process has been aborted.</param> + public async Task StartAsync(CancellationToken cancellationToken) { + _listenerTask = Run(cancellationToken); + logger.LogInformation("Bot started!"); + } + + private async Task Run(CancellationToken cancellationToken) { + if (Directory.Exists("bot_data/cache")) + Directory.GetFiles("bot_data/cache").ToList().ForEach(File.Delete); + + BotData botData; + + try { + logger.LogInformation("Fetching bot account data..."); + botData = await hs.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data"); + logger.LogInformation("Got bot account data..."); + } + catch (Exception e) { + logger.LogInformation("Could not fetch bot account data... {}", e.Message); + if (e is not MatrixException { ErrorCode: "M_NOT_FOUND" }) { + logger.LogError("{}", e.ToString()); + throw; + } + + botData = null; + } + + botData = await FirstRunTasks.ConstructBotData(hs, configuration, botData); + + // _policyRoom = hs.GetRoom(botData.PolicyRoom ?? botData.ControlRoom); + _logRoom = hs.GetRoom(botData.LogRoom ?? botData.ControlRoom); + _controlRoom = hs.GetRoom(botData.ControlRoom); + foreach (var configurationAdmin in configuration.Admins) { + var pls = await _controlRoom.GetPowerLevelsAsync(); + if (pls is null) { + await _logRoom?.SendMessageEventAsync(MessageFormatter.FormatWarning($"Control room has no m.room.power_levels?")); + continue; + } + pls.SetUserPowerLevel(configurationAdmin, pls.GetUserPowerLevel(hs.UserId)); + await _controlRoom.SendStateEventAsync(RoomPowerLevelEventContent.EventId, pls); + } + var syncHelper = new SyncHelper(hs); + + List<string> admins = new(); + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(async () => { + while (!cancellationToken.IsCancellationRequested) { + var controlRoomMembers = _controlRoom.GetMembersEnumerableAsync(); + var pls = await _controlRoom.GetPowerLevelsAsync(); + await foreach (var member in controlRoomMembers) { + if ((member.TypedContent as RoomMemberEventContent)? + .Membership == "join" && pls.UserHasTimelinePermission(member.Sender, RoomMessageEventContent.EventId)) admins.Add(member.StateKey); + } + + await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken); + } + }, cancellationToken); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + syncHelper.InviteReceivedHandlers.Add(async Task (args) => { + var inviteEvent = + args.Value.InviteState.Events.FirstOrDefault(x => + x.Type == "m.room.member" && x.StateKey == hs.UserId); + logger.LogInformation("Got invite to {RoomId} by {Sender} with reason: {Reason}", args.Key, inviteEvent!.Sender, + (inviteEvent.TypedContent as RoomMemberEventContent)!.Reason); + await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Bot invited to {MessageFormatter.HtmlFormatMention(args.Key)} by {MessageFormatter.HtmlFormatMention(inviteEvent.Sender)}")); + if (admins.Contains(inviteEvent.Sender)) { + try { + await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joining {MessageFormatter.HtmlFormatMention(args.Key)}...")); + var senderProfile = await hs.GetProfileAsync(inviteEvent.Sender); + await hs.GetRoom(args.Key).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!"); + } + catch (Exception e) { + logger.LogError("{}", e.ToString()); + await _logRoom.SendMessageEventAsync(MessageFormatter.FormatException("Could not join room", e)); + await hs.GetRoom(args.Key).LeaveAsync(reason: "I was unable to join the room: " + e); + } + } + }); + + syncHelper.TimelineEventHandlers.Add(async @event => { + var room = hs.GetRoom(@event.RoomId); + try { + logger.LogInformation( + "Got timeline event in {}: {}", @event.RoomId, @event.ToJson(indent: true, ignoreNull: true)); + + if (@event != null && ( + @event.MappedType.IsAssignableTo(typeof(BasePolicy)) + || @event.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent)) + )) { + await LogPolicyChange(@event); + await engine.ReloadActivePolicyListById(@event.RoomId); + } + + var rules = await engine.GetMatchingPolicies(@event); + foreach (var matchedRule in rules) { + await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccessJson( + $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.OriginalEvent.EventId, roomId: matchedRule.PolicyList.Room.RoomId, displayName: "rule")}", @matchedRule.OriginalEvent.RawContent)); + } + + if (configuration.DemoMode) { + // foreach (var matchedRule in rules) { + // await room.SendMessageEventAsync(MessageFormatter.FormatSuccessJson( + // $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.EventId, roomId: matchedRule.RoomId, displayName: "rule")}", @matchedRule.RawContent)); + // } + return; + } + // + // if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventContent message }) { + // if (message is { MessageType: "m.image" }) { + // //check media + // // var matchedPolicy = await CheckMedia(@event); + // var matchedPolicy = rules.FirstOrDefault(); + // if (matchedPolicy is null) return; + // var matchedpolicyData = matchedPolicy.TypedContent as MediaPolicyEventContent; + // await _logRoom.SendMessageEventAsync( + // new RoomMessageEventContent( + // body: + // $"User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: {matchedPolicy.RawContent!.ToJson(ignoreNull: true)}", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = + // $"<font color=\"#FFFF00\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: <pre>{matchedPolicy.RawContent!.ToJson(ignoreNull: true)}</pre></font>" + // }); + // switch (matchedpolicyData.Recommendation) { + // case "warn_admins": { + // await _controlRoom.SendMessageEventAsync( + // new RoomMessageEventContent( + // body: $"{string.Join(' ', admins)}\nUser {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image {message.Url}", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = $"{string.Join(' ', admins.Select(u => MessageFormatter.HtmlFormatMention(u)))}\n" + + // $"<font color=\"#FF0000\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image <a href=\"{message.Url}\">{message.Url}</a></font>" + // }); + // break; + // } + // case "warn": { + // await room.SendMessageEventAsync( + // new RoomMessageEventContent( + // body: $"Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = + // $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}</a></font>" + // }); + // break; + // } + // case "redact": { + // await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason ?? "No reason specified"); + // break; + // } + // case "spoiler": { + // // <blockquote> + // // <a href=\"https://matrix.to/#/@emma:rory.gay\">@emma:rory.gay</a><br> + // // <a href=\"https://codeberg.org/crimsonfork/CN\"></a> + // // <font color=\"#dc143c\" data-mx-color=\"#dc143c\"> + // // <b>CN</b> + // // </font>: + // // <a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\">test</a><br> + // // <span data-mx-spoiler=\"\"><a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\"> + // // <img src=\"mxc://rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\" height=\"69\"></a> + // // </span> + // // </blockquote> + // await room.SendMessageEventAsync( + // new RoomMessageEventContent( + // body: + // $"Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = + // $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:</a></font>" + // }); + // var imageUrl = message.Url; + // await room.SendMessageEventAsync( + // new RoomMessageEventContent(body: $"CN: {imageUrl}", + // messageType: "m.text") { + // Format = "org.matrix.custom.html", + // FormattedBody = $""" + // <blockquote> + // <font color=\"#dc143c\" data-mx-color=\"#dc143c\"> + // <b>CN</b> + // </font>: + // <a href=\"{imageUrl}\">{matchedpolicyData.Reason}</a><br> + // <span data-mx-spoiler=\"\"> + // <a href=\"{imageUrl}\"> + // <img src=\"{imageUrl}\" height=\"69\"> + // </a> + // </span> + // </blockquote> + // """ + // }); + // await room.RedactEventAsync(@event.EventId, "Automatically spoilered: " + matchedpolicyData.Reason); + // break; + // } + // case "mute": { + // await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + // //change powerlevel to -1 + // var currentPls = await room.GetPowerLevelsAsync(); + // if (currentPls is null) { + // logger.LogWarning("Unable to get power levels for {room}", room.RoomId); + // await _logRoom.SendMessageEventAsync( + // MessageFormatter.FormatError($"Unable to get power levels for {MessageFormatter.HtmlFormatMention(room.RoomId)}")); + // return; + // } + // + // currentPls.Users ??= new(); + // currentPls.Users[@event.Sender] = -1; + // await room.SendStateEventAsync("m.room.power_levels", currentPls); + // break; + // } + // case "kick": { + // await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + // await room.KickAsync(@event.Sender, matchedpolicyData.Reason); + // break; + // } + // case "ban": { + // await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + // await room.BanAsync(@event.Sender, matchedpolicyData.Reason); + // break; + // } + // default: { + // throw new ArgumentOutOfRangeException("recommendation", + // $"Unknown response type {matchedpolicyData.Recommendation}!"); + // } + // } + // } + // } + } + catch (Exception e) { + logger.LogError("{}", e.ToString()); + await _controlRoom.SendMessageEventAsync( + MessageFormatter.FormatException($"Unable to process event in {MessageFormatter.HtmlFormatMention(room.RoomId)}", e)); + await _logRoom.SendMessageEventAsync( + MessageFormatter.FormatException($"Unable to process event in {MessageFormatter.HtmlFormatMention(room.RoomId)}", e)); + await using var stream = new MemoryStream(e.ToString().AsBytes().ToArray()); + await _controlRoom.SendFileAsync("error.log.cs", stream); + await _logRoom.SendFileAsync("error.log.cs", stream); + } + }); + await engine.ReloadActivePolicyLists(); + await syncHelper.RunSyncLoopAsync(); + } + + private async Task LogPolicyChange(StateEventResponse changeEvent) { + var room = hs.GetRoom(changeEvent.RoomId!); + var message = MessageFormatter.FormatWarning($"Policy change detected in {MessageFormatter.HtmlFormatMessageLink(changeEvent.RoomId, changeEvent.EventId, [hs.ServerName], await room.GetNameOrFallbackAsync())}!"); + message = message.ConcatLine(new RoomMessageEventContent(body: $"Policy type: {changeEvent.Type} -> {changeEvent.MappedType.Name}") { + FormattedBody = $"Policy type: {changeEvent.Type} -> {changeEvent.MappedType.Name}" + }); + var isUpdated = changeEvent.Unsigned.PrevContent is { Count: > 0 }; + var isRemoved = changeEvent.RawContent is not { Count: > 0 }; + // if (isUpdated) { + // message = message.ConcatLine(MessageFormatter.FormatSuccess("Rule updated!")); + // message = message.ConcatLine(MessageFormatter.FormatSuccessJson("Old rule:", changeEvent.Unsigned.PrevContent!)); + // } + // else if (isRemoved) { + // message = message.ConcatLine(MessageFormatter.FormatWarningJson("Rule removed!", changeEvent.Unsigned.PrevContent!)); + // } + // else { + // message = message.ConcatLine(MessageFormatter.FormatSuccess("New rule added!")); + // } + message = message.ConcatLine(MessageFormatter.FormatSuccessJson($"{(isUpdated ? "Updated" : isRemoved ? "Removed" : "New")} rule: {changeEvent.StateKey}", changeEvent.RawContent!)); + if (isRemoved || isUpdated) { + message = message.ConcatLine(MessageFormatter.FormatSuccessJson("Old content: ", changeEvent.Unsigned.PrevContent!)); + } + + await _logRoom.SendMessageEventAsync(message); + } + + /// <summary>Triggered when the application host is performing a graceful shutdown.</summary> + /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param> + public async Task StopAsync(CancellationToken cancellationToken) { + logger.LogInformation("Shutting down bot!"); + } + +} diff --git a/MatrixUtils.DmSpaced/ModerationBotConfiguration.cs b/MatrixUtils.DmSpaced/ModerationBotConfiguration.cs new file mode 100644 index 0000000..47e6423 --- /dev/null +++ b/MatrixUtils.DmSpaced/ModerationBotConfiguration.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Configuration; + +namespace ModerationBot; + +public class ModerationBotConfiguration { + public ModerationBotConfiguration(IConfiguration config) => config.GetRequiredSection("ModerationBot").Bind(this); + + public string Homeserver { get; set; } + public string AccessToken { get; set; } +} diff --git a/MatrixUtils.DmSpaced/Program.cs b/MatrixUtils.DmSpaced/Program.cs new file mode 100644 index 0000000..ae352b7 --- /dev/null +++ b/MatrixUtils.DmSpaced/Program.cs @@ -0,0 +1,40 @@ +// See https://aka.ms/new-console-template for more information + +using LibMatrix.Services; +using LibMatrix.Utilities.Bot; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using ModerationBot; +using ModerationBot.Services; + +Console.WriteLine("Hello, World!"); + +var builder = Host.CreateDefaultBuilder(args); + +builder.ConfigureHostOptions(host => { + host.ServicesStartConcurrently = true; + host.ServicesStopConcurrently = true; + host.ShutdownTimeout = TimeSpan.FromSeconds(5); +}); + +if (Environment.GetEnvironmentVariable("MODERATIONBOT_APPSETTINGS_PATH") is string path) + builder.ConfigureAppConfiguration(x => x.AddJsonFile(path)); + +var host = builder.ConfigureServices((_, services) => { + services.AddScoped<TieredStorageService>(x => + new TieredStorageService( + cacheStorageProvider: new FileStorageProvider("bot_data/cache/"), + dataStorageProvider: new FileStorageProvider("bot_data/data/") + ) + ); + services.AddSingleton<ModerationBotConfiguration>(); + + services.AddRoryLibMatrixServices(); + + services.AddSingleton<ModerationBotRoomProvider>(); + + services.AddHostedService<ModerationBot.ModerationBot>(); +}).UseConsoleLifetime().Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/MatrixUtils.DmSpaced/appsettings.Development.json b/MatrixUtils.DmSpaced/appsettings.Development.json new file mode 100644 index 0000000..224d0da --- /dev/null +++ b/MatrixUtils.DmSpaced/appsettings.Development.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "LibMatrixBot": { + // The homeserver to connect to + "Homeserver": "rory.gay", + // The access token to use + "AccessToken": "syt_xxxxxxxxxxxxxxxxx", + // The command prefix + "Prefix": "?" + }, + "ModerationBot": { + // List of people who should be invited to the control room + "Admins": [ + "@emma:conduit.rory.gay", + "@emma:rory.gay" + ] + } +} diff --git a/MatrixUtils.DmSpaced/appsettings.json b/MatrixUtils.DmSpaced/appsettings.json new file mode 100644 index 0000000..6ba02f3 --- /dev/null +++ b/MatrixUtils.DmSpaced/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} |