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