{ config, lib, pkgs, ... }: let cfg = config.services.akkoma.moderation; reason = lib.mkOption { type = lib.types.nonEmptyStr; description = '' Explanation for moderation decision. ''; }; in { options.services.akkoma.moderation = { instances = lib.mkOption { type = with lib.types; attrsOf (submodule { options = { inherit reason; activities = lib.mkOption { type = with lib.types; (nullOr (enum [ "unlist" "restrict" "reject" "followersOnly" ])); default = null; description = '' Activity moderation: - `unlist`: Remove activities from federated timeline. - `restrict`: Force activities to be visible to followers only. - `reject`: Reject all activities except deletes. ''; }; media = lib.mkOption { type = with lib.types; (nullOr (enum [ "mark" "strip" ])); default = null; description = '' Media attachment moderation: - `mark`: Mark media attachments as sensitive. - `strip`: Strip all media attachments. ''; }; }; }); description = '' Instance moderations. ''; default = { }; example = { "reallybadinstance.social" = { reason = "Persistent harrassment of users, no effective moderation."; activities = "reject"; }; "verylewd.xxx" = { reason = "Lots of untagged lewd media."; activities = "unlist"; media = "mark"; }; }; }; hashtags = lib.mkOption { type = with lib.types; attrsOf (submodule { options = { inherit reason; sensitive = lib.mkOption { type = lib.types.bool; default = false; description = '' Mark tagged activities as sensitive. ''; }; unlisted = lib.mkOption { type = lib.types.bool; default = false; description = '' Remove tagged activities from federated timeline. ''; }; }; }); description = '' Hashtag moderations. ''; default = { }; example = { "lewd" = { reason = "Reduce visibility of activities tagged as lewd."; sensitive = true; unlisted = true; }; }; }; keywords = lib.mkOption { type = with lib.types; attrsOf (submodule { options = { inherit reason; action = lib.mkOption { type = lib.types.bool; description = '' Activity moderation: - `unlist`: Remove matching activities from federated timelines. - `reject`: Reject matching activities. ''; }; }; }); description = '' Keyword moderations. ''; default = { }; example = { "/all hail( our glorious leader)? hypnotoad/i" = { reason = "Reduce influence of Hypnotoad on our users."; action = "unlist"; }; }; }; unknownUserDelay = lib.mkOption { type = with lib.types; nullOr ints.unsigned; default = 900; description = '' After initially encountering a user, reject all their posts for the configured number of seconds. Set to `null` to disable. ''; }; hellthread = { delist = lib.mkOption { type = lib.types.ints.unsigned; default = 8; description = '' Number of mentioned users beyond which a message gets delisted. It will not show up in public timelines and mentioned users will not get notified. ''; }; reject = lib.mkOption { type = lib.types.ints.unsigned; default = 16; description = '' Number of mentioned users beyond which a message will be rejected. ''; }; }; }; config.services.akkoma.config.":pleroma" = let elixir = pkgs.formats.elixirConf { }; in { ":mrf" = { transparency = lib.mkDefault true; policies = map elixir.lib.mkRaw [ "Pleroma.Web.ActivityPub.MRF.SimplePolicy" "Pleroma.Web.ActivityPub.MRF.KeywordPolicy" "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy" "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy" # Spam reduction "Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy" "Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy" # Local policies "Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy" "Pleroma.Web.ActivityPub.MRF.TagPolicy" # Local userā€specific moderation # Always enabled: # Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy # Pleroma.Web.ActivityPub.MRF.HashtagPolicy # Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy # Pleroma.Web.ActivityPub.MRF.NormalizeMarkup ]; }; ":mrf_simple" = let rules = fun: attrs: lib.mapAttrsToList (name: rule: elixir.lib.mkTuple [ name rule.reason ]) (lib.filterAttrs (name: rule: fun rule) attrs); in { media_removal = rules (rule: rule.media == "strip") cfg.instances; media_nsfw = rules (rule: rule.media == "mark") cfg.instances; federated_timeline_removal = rules (rule: rule.activities == "unlist") cfg.instances; followers_only = rules (rule: rule.activities == "followersOnly") cfg.instances; reject = rules (rule: rule.activities == "reject") cfg.instances; }; ":mrf_hashtag" = let rules = fun: attrs: lib.attrNames (lib.filterAttrs (name: rule: fun rule) attrs); in { sensitive = rules (rule: rule.sensitive) cfg.hashtags; federated_timeline_removal = rules (rule: rule.unlisted) cfg.hashtags; }; ":mrf_keyword" = let rules = fun: attrs: lib.mapAttrsToList (name: rule: elixir.lib.mkRaw "~r${name}") (lib.filterAttrs (name: rule: fun rule) attrs); in { reject = rules (rule: rule.action == "reject") cfg.keywords; federated_timeline_removal = rules (rule: rule.action == "unlist") cfg.keywords; }; ":mrf_hellthread" = { delist_threshold = cfg.hellthread.delist; reject_threshold = cfg.hellthread.reject; }; ":mrf_object_age".threshold = lib.mkDefault (90 * 24 * 3600); # 90 days ":mrf_reject_newly_created_account_notes".age = lib.mkIf (cfg.unknownUserDelay != null) cfg.unknownUserDelay; }; }