at 23.11-pre 8.2 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4let 5 cfg = config.services.mjolnir; 6 7 yamlConfig = { 8 inherit (cfg) dataPath managementRoom protectedRooms; 9 10 accessToken = "@ACCESS_TOKEN@"; # will be replaced in "generateConfig" 11 homeserverUrl = 12 if cfg.pantalaimon.enable then 13 "http://${cfg.pantalaimon.options.listenAddress}:${toString cfg.pantalaimon.options.listenPort}" 14 else 15 cfg.homeserverUrl; 16 17 rawHomeserverUrl = cfg.homeserverUrl; 18 19 pantalaimon = { 20 inherit (cfg.pantalaimon) username; 21 22 use = cfg.pantalaimon.enable; 23 password = "@PANTALAIMON_PASSWORD@"; # will be replaced in "generateConfig" 24 }; 25 }; 26 27 moduleConfigFile = pkgs.writeText "module-config.yaml" ( 28 generators.toYAML { } (filterAttrs (_: v: v != null) 29 (fold recursiveUpdate { } [ yamlConfig cfg.settings ]))); 30 31 # these config files will be merged one after the other to build the final config 32 configFiles = [ 33 "${pkgs.mjolnir}/libexec/mjolnir/deps/mjolnir/config/default.yaml" 34 moduleConfigFile 35 ]; 36 37 # this will generate the default.yaml file with all configFiles as inputs and 38 # replace all secret strings using replace-secret 39 generateConfig = pkgs.writeShellScript "mjolnir-generate-config" ( 40 let 41 yqEvalStr = concatImapStringsSep " * " (pos: _: "select(fileIndex == ${toString (pos - 1)})") configFiles; 42 yqEvalArgs = concatStringsSep " " configFiles; 43 in 44 '' 45 set -euo pipefail 46 47 umask 077 48 49 # mjolnir will try to load a config from "./config/default.yaml" in the working directory 50 # -> let's place the generated config there 51 mkdir -p ${cfg.dataPath}/config 52 53 # merge all config files into one, overriding settings of the previous one with the next config 54 # e.g. "eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' filea.yaml fileb.yaml" will merge filea.yaml with fileb.yaml 55 ${pkgs.yq-go}/bin/yq eval-all -P '${yqEvalStr}' ${yqEvalArgs} > ${cfg.dataPath}/config/default.yaml 56 57 ${optionalString (cfg.accessTokenFile != null) '' 58 ${pkgs.replace-secret}/bin/replace-secret '@ACCESS_TOKEN@' '${cfg.accessTokenFile}' ${cfg.dataPath}/config/default.yaml 59 ''} 60 ${optionalString (cfg.pantalaimon.passwordFile != null) '' 61 ${pkgs.replace-secret}/bin/replace-secret '@PANTALAIMON_PASSWORD@' '${cfg.pantalaimon.passwordFile}' ${cfg.dataPath}/config/default.yaml 62 ''} 63 '' 64 ); 65in 66{ 67 options.services.mjolnir = { 68 enable = mkEnableOption (lib.mdDoc "Mjolnir, a moderation tool for Matrix"); 69 70 homeserverUrl = mkOption { 71 type = types.str; 72 default = "https://matrix.org"; 73 description = lib.mdDoc '' 74 Where the homeserver is located (client-server URL). 75 76 If `pantalaimon.enable` is `true`, this option will become the homeserver to which `pantalaimon` connects. 77 The listen address of `pantalaimon` will then become the `homeserverUrl` of `mjolnir`. 78 ''; 79 }; 80 81 accessTokenFile = mkOption { 82 type = with types; nullOr path; 83 default = null; 84 description = lib.mdDoc '' 85 File containing the matrix access token for the `mjolnir` user. 86 ''; 87 }; 88 89 pantalaimon = mkOption { 90 description = lib.mdDoc '' 91 `pantalaimon` options (enables E2E Encryption support). 92 93 This will create a `pantalaimon` instance with the name "mjolnir". 94 ''; 95 default = { }; 96 type = types.submodule { 97 options = { 98 enable = mkEnableOption (lib.mdDoc '' 99 If true, accessToken is ignored and the username/password below will be 100 used instead. The access token of the bot will be stored in the dataPath. 101 ''); 102 103 username = mkOption { 104 type = types.str; 105 description = lib.mdDoc "The username to login with."; 106 }; 107 108 passwordFile = mkOption { 109 type = with types; nullOr path; 110 default = null; 111 description = lib.mdDoc '' 112 File containing the matrix password for the `mjolnir` user. 113 ''; 114 }; 115 116 options = mkOption { 117 type = types.submodule (import ./pantalaimon-options.nix); 118 default = { }; 119 description = lib.mdDoc '' 120 passthrough additional options to the `pantalaimon` service. 121 ''; 122 }; 123 }; 124 }; 125 }; 126 127 dataPath = mkOption { 128 type = types.path; 129 default = "/var/lib/mjolnir"; 130 description = lib.mdDoc '' 131 The directory the bot should store various bits of information in. 132 ''; 133 }; 134 135 managementRoom = mkOption { 136 type = types.str; 137 default = "#moderators:example.org"; 138 description = lib.mdDoc '' 139 The room ID where people can use the bot. The bot has no access controls, so 140 anyone in this room can use the bot - secure your room! 141 This should be a room alias or room ID - not a matrix.to URL. 142 Note: `mjolnir` is fairly verbose - expect a lot of messages from it. 143 ''; 144 }; 145 146 protectedRooms = mkOption { 147 type = types.listOf types.str; 148 default = [ ]; 149 example = literalExpression '' 150 [ 151 "https://matrix.to/#/#yourroom:example.org" 152 "https://matrix.to/#/#anotherroom:example.org" 153 ] 154 ''; 155 description = lib.mdDoc '' 156 A list of rooms to protect (matrix.to URLs). 157 ''; 158 }; 159 160 settings = mkOption { 161 default = { }; 162 type = (pkgs.formats.yaml { }).type; 163 example = literalExpression '' 164 { 165 autojoinOnlyIfManager = true; 166 automaticallyRedactForReasons = [ "spam" "advertising" ]; 167 } 168 ''; 169 description = lib.mdDoc '' 170 Additional settings (see [mjolnir default config](https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml) for available settings). These settings will override settings made by the module config. 171 ''; 172 }; 173 }; 174 175 config = mkIf config.services.mjolnir.enable { 176 assertions = [ 177 { 178 assertion = !(cfg.pantalaimon.enable && cfg.pantalaimon.passwordFile == null); 179 message = "Specify pantalaimon.passwordFile"; 180 } 181 { 182 assertion = !(cfg.pantalaimon.enable && cfg.accessTokenFile != null); 183 message = "Do not specify accessTokenFile when using pantalaimon"; 184 } 185 { 186 assertion = !(!cfg.pantalaimon.enable && cfg.accessTokenFile == null); 187 message = "Specify accessTokenFile when not using pantalaimon"; 188 } 189 ]; 190 191 services.pantalaimon-headless.instances."mjolnir" = mkIf cfg.pantalaimon.enable 192 { 193 homeserver = cfg.homeserverUrl; 194 } // cfg.pantalaimon.options; 195 196 systemd.services.mjolnir = { 197 description = "mjolnir - a moderation tool for Matrix"; 198 wants = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ]; 199 after = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ]; 200 wantedBy = [ "multi-user.target" ]; 201 202 serviceConfig = { 203 ExecStart = ''${pkgs.mjolnir}/bin/mjolnir --mjolnir-config ./config/default.yaml''; 204 ExecStartPre = [ generateConfig ]; 205 WorkingDirectory = cfg.dataPath; 206 StateDirectory = "mjolnir"; 207 StateDirectoryMode = "0700"; 208 ProtectSystem = "strict"; 209 ProtectHome = true; 210 PrivateTmp = true; 211 NoNewPrivileges = true; 212 PrivateDevices = true; 213 User = "mjolnir"; 214 Restart = "on-failure"; 215 216 /* TODO: wait for #102397 to be resolved. Then load secrets from $CREDENTIALS_DIRECTORY+"/NAME" 217 DynamicUser = true; 218 LoadCredential = [] ++ 219 optionals (cfg.accessTokenFile != null) [ 220 "access_token:${cfg.accessTokenFile}" 221 ] ++ 222 optionals (cfg.pantalaimon.passwordFile != null) [ 223 "pantalaimon_password:${cfg.pantalaimon.passwordFile}" 224 ]; 225 */ 226 }; 227 }; 228 229 users = { 230 users.mjolnir = { 231 group = "mjolnir"; 232 isSystemUser = true; 233 }; 234 groups.mjolnir = { }; 235 }; 236 }; 237 238 meta = { 239 doc = ./mjolnir.md; 240 maintainers = with maintainers; [ jojosch ]; 241 }; 242}