at 25.11-pre 25 kB view raw
1{ 2 config, 3 pkgs, 4 lib, 5 ... 6}: 7 8let 9 settingsFormat = pkgs.formats.yaml { }; 10 11 upperConfig = config; 12 cfg = config.services.mautrix-meta; 13 upperCfg = cfg; 14 15 fullDataDir = cfg: "/var/lib/${cfg.dataDir}"; 16 17 settingsFile = cfg: "${fullDataDir cfg}/config.yaml"; 18 settingsFileUnsubstituted = cfg: settingsFormat.generate "mautrix-meta-config.yaml" cfg.settings; 19 20 metaName = name: "mautrix-meta-${name}"; 21 22 enabledInstances = lib.filterAttrs ( 23 name: config: config.enable 24 ) config.services.mautrix-meta.instances; 25 registerToSynapseInstances = lib.filterAttrs ( 26 name: config: config.enable && config.registerToSynapse 27 ) config.services.mautrix-meta.instances; 28in 29{ 30 options = { 31 services.mautrix-meta = { 32 33 package = lib.mkPackageOption pkgs "mautrix-meta" { }; 34 35 instances = lib.mkOption { 36 type = lib.types.attrsOf ( 37 lib.types.submodule ( 38 { config, name, ... }: 39 { 40 41 options = { 42 43 enable = lib.mkEnableOption "Mautrix-Meta, a Matrix <-> Facebook and Matrix <-> Instagram hybrid puppeting/relaybot bridge"; 44 45 dataDir = lib.mkOption { 46 type = lib.types.str; 47 default = metaName name; 48 description = '' 49 Path to the directory with database, registration, and other data for the bridge service. 50 This path is relative to `/var/lib`, it cannot start with `../` (it cannot be outside of `/var/lib`). 51 ''; 52 }; 53 54 registrationFile = lib.mkOption { 55 type = lib.types.path; 56 readOnly = true; 57 description = '' 58 Path to the yaml registration file of the appservice. 59 ''; 60 }; 61 62 registerToSynapse = lib.mkOption { 63 type = lib.types.bool; 64 default = true; 65 description = '' 66 Whether to add registration file to `services.matrix-synapse.settings.app_service_config_files` and 67 make Synapse wait for registration service. 68 ''; 69 }; 70 71 settings = lib.mkOption rec { 72 apply = lib.recursiveUpdate default; 73 inherit (settingsFormat) type; 74 default = { 75 homeserver = { 76 software = "standard"; 77 78 domain = ""; 79 address = ""; 80 }; 81 82 appservice = { 83 id = ""; 84 85 bot = { 86 username = ""; 87 }; 88 89 hostname = "localhost"; 90 port = 29319; 91 address = "http://${config.settings.appservice.hostname}:${toString config.settings.appservice.port}"; 92 }; 93 94 bridge = { 95 permissions = { }; 96 }; 97 98 database = { 99 type = "sqlite3-fk-wal"; 100 uri = "file:${fullDataDir config}/mautrix-meta.db?_txlock=immediate"; 101 }; 102 103 # Enable encryption by default to make the bridge more secure 104 encryption = { 105 allow = true; 106 default = true; 107 require = true; 108 109 # Recommended options from mautrix documentation 110 # for additional security. 111 delete_keys = { 112 dont_store_outbound = true; 113 ratchet_on_decrypt = true; 114 delete_fully_used_on_decrypt = true; 115 delete_prev_on_new_session = true; 116 delete_on_device_delete = true; 117 periodically_delete_expired = true; 118 delete_outdated_inbound = true; 119 }; 120 121 # TODO: This effectively disables encryption. But this is the value provided when a <0.4 config is migrated. Changing it will corrupt the database. 122 # https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L24 123 # If you wish to encrypt the local database you should set this to an environment variable substitution and reset the bridge or somehow migrate the DB. 124 pickle_key = "mautrix.bridge.e2ee"; 125 126 verification_levels = { 127 receive = "cross-signed-tofu"; 128 send = "cross-signed-tofu"; 129 share = "cross-signed-tofu"; 130 }; 131 }; 132 133 logging = { 134 min_level = "info"; 135 writers = lib.singleton { 136 type = "stdout"; 137 format = "pretty-colored"; 138 time_format = " "; 139 }; 140 }; 141 142 network = { 143 mode = ""; 144 }; 145 }; 146 defaultText = '' 147 { 148 homeserver = { 149 software = "standard"; 150 address = "https://''${config.settings.homeserver.domain}"; 151 }; 152 153 appservice = { 154 database = { 155 type = "sqlite3-fk-wal"; 156 uri = "file:''${fullDataDir config}/mautrix-meta.db?_txlock=immediate"; 157 }; 158 159 hostname = "localhost"; 160 port = 29319; 161 address = "http://''${config.settings.appservice.hostname}:''${toString config.settings.appservice.port}"; 162 }; 163 164 bridge = { 165 # Require encryption by default to make the bridge more secure 166 encryption = { 167 allow = true; 168 default = true; 169 require = true; 170 171 # Recommended options from mautrix documentation 172 # for optimal security. 173 delete_keys = { 174 dont_store_outbound = true; 175 ratchet_on_decrypt = true; 176 delete_fully_used_on_decrypt = true; 177 delete_prev_on_new_session = true; 178 delete_on_device_delete = true; 179 periodically_delete_expired = true; 180 delete_outdated_inbound = true; 181 }; 182 183 verification_levels = { 184 receive = "cross-signed-tofu"; 185 send = "cross-signed-tofu"; 186 share = "cross-signed-tofu"; 187 }; 188 }; 189 }; 190 191 logging = { 192 min_level = "info"; 193 writers = lib.singleton { 194 type = "stdout"; 195 format = "pretty-colored"; 196 time_format = " "; 197 }; 198 }; 199 }; 200 ''; 201 description = '' 202 {file}`config.yaml` configuration as a Nix attribute set. 203 Configuration options should match those described in 204 [example-config.yaml](https://github.com/mautrix/meta/blob/main/example-config.yaml). 205 206 Secret tokens should be specified using {option}`environmentFile` 207 instead 208 ''; 209 }; 210 211 environmentFile = lib.mkOption { 212 type = lib.types.nullOr lib.types.path; 213 default = null; 214 description = '' 215 File containing environment variables to substitute when copying the configuration 216 out of Nix store to the `services.mautrix-meta.dataDir`. 217 218 Can be used for storing the secrets without making them available in the Nix store. 219 220 For example, you can set `services.mautrix-meta.settings.appservice.as_token = "$MAUTRIX_META_APPSERVICE_AS_TOKEN"` 221 and then specify `MAUTRIX_META_APPSERVICE_AS_TOKEN="{token}"` in the environment file. 222 This value will get substituted into the configuration file as as token. 223 ''; 224 }; 225 226 serviceDependencies = lib.mkOption { 227 type = lib.types.listOf lib.types.str; 228 default = 229 [ config.registrationServiceUnit ] 230 ++ (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) 231 ++ (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") 232 ++ (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service"); 233 234 defaultText = '' 235 [ config.registrationServiceUnit ] ++ 236 (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) ++ 237 (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") ++ 238 (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service"); 239 ''; 240 description = '' 241 List of Systemd services to require and wait for when starting the application service. 242 ''; 243 }; 244 245 serviceUnit = lib.mkOption { 246 type = lib.types.str; 247 readOnly = true; 248 description = '' 249 The systemd unit (a service or a target) for other services to depend on if they 250 need to be started after matrix-synapse. 251 252 This option is useful as the actual parent unit for all matrix-synapse processes 253 changes when configuring workers. 254 ''; 255 }; 256 257 registrationServiceUnit = lib.mkOption { 258 type = lib.types.str; 259 readOnly = true; 260 description = '' 261 The registration service that generates the registration file. 262 263 Systemd unit (a service or a target) for other services to depend on if they 264 need to be started after mautrix-meta registration service. 265 266 This option is useful as the actual parent unit for all matrix-synapse processes 267 changes when configuring workers. 268 ''; 269 }; 270 }; 271 272 config = { 273 serviceUnit = (metaName name) + ".service"; 274 registrationServiceUnit = (metaName name) + "-registration.service"; 275 registrationFile = (fullDataDir config) + "/meta-registration.yaml"; 276 }; 277 } 278 ) 279 ); 280 281 description = '' 282 Configuration of multiple `mautrix-meta` instances. 283 `services.mautrix-meta.instances.facebook` and `services.mautrix-meta.instances.instagram` 284 come preconfigured with network.mode, appservice.id, bot username, display name and avatar. 285 ''; 286 287 example = '' 288 { 289 facebook = { 290 enable = true; 291 settings = { 292 homeserver.domain = "example.com"; 293 }; 294 }; 295 296 instagram = { 297 enable = true; 298 settings = { 299 homeserver.domain = "example.com"; 300 }; 301 }; 302 303 messenger = { 304 enable = true; 305 settings = { 306 network.mode = "messenger"; 307 homeserver.domain = "example.com"; 308 appservice = { 309 id = "messenger"; 310 bot = { 311 username = "messengerbot"; 312 displayname = "Messenger bridge bot"; 313 avatar = "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak"; 314 }; 315 }; 316 }; 317 }; 318 } 319 ''; 320 }; 321 }; 322 }; 323 324 config = lib.mkMerge [ 325 (lib.mkIf (enabledInstances != { }) { 326 assertions = lib.mkMerge ( 327 lib.attrValues ( 328 lib.mapAttrs (name: cfg: [ 329 { 330 assertion = cfg.settings.homeserver.domain != "" && cfg.settings.homeserver.address != ""; 331 message = '' 332 The options with information about the homeserver: 333 `services.mautrix-meta.instances.${name}.settings.homeserver.domain` and 334 `services.mautrix-meta.instances.${name}.settings.homeserver.address` have to be set. 335 ''; 336 } 337 { 338 assertion = builtins.elem cfg.settings.network.mode [ 339 "facebook" 340 "facebook-tor" 341 "messenger" 342 "instagram" 343 ]; 344 message = '' 345 The option `services.mautrix-meta.instances.${name}.settings.network.mode` has to be set 346 to one of: facebook, facebook-tor, messenger, instagram. 347 This configures the mode of the bridge. 348 ''; 349 } 350 { 351 assertion = cfg.settings.bridge.permissions != { }; 352 message = '' 353 The option `services.mautrix-meta.instances.${name}.settings.bridge.permissions` has to be set. 354 ''; 355 } 356 { 357 assertion = cfg.settings.appservice.id != ""; 358 message = '' 359 The option `services.mautrix-meta.instances.${name}.settings.appservice.id` has to be set. 360 ''; 361 } 362 { 363 assertion = cfg.settings.appservice.bot.username != ""; 364 message = '' 365 The option `services.mautrix-meta.instances.${name}.settings.appservice.bot.username` has to be set. 366 ''; 367 } 368 { 369 assertion = !(cfg.settings ? bridge.disable_xma); 370 message = '' 371 The option `bridge.disable_xma` has been moved to `network.disable_xma_always`. Please [migrate your configuration](https://github.com/mautrix/meta/releases/tag/v0.4.0). You may wish to use [the auto-migration code](https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L23) for reference. 372 ''; 373 } 374 { 375 assertion = !(cfg.settings ? bridge.displayname_template); 376 message = '' 377 The option `bridge.displayname_template` has been moved to `network.displayname_template`. Please [migrate your configuration](https://github.com/mautrix/meta/releases/tag/v0.4.0). You may wish to use [the auto-migration code](https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L23) for reference. 378 ''; 379 } 380 { 381 assertion = !(cfg.settings ? meta); 382 message = '' 383 The options in `meta` have been moved to `network`. Please [migrate your configuration](https://github.com/mautrix/meta/releases/tag/v0.4.0). You may wish to use [the auto-migration code](https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L23) for reference. 384 ''; 385 } 386 ]) enabledInstances 387 ) 388 ); 389 390 users.users = lib.mapAttrs' ( 391 name: cfg: 392 lib.nameValuePair "mautrix-meta-${name}" { 393 isSystemUser = true; 394 group = "mautrix-meta"; 395 extraGroups = [ "mautrix-meta-registration" ]; 396 description = "Mautrix-Meta-${name} bridge user"; 397 } 398 ) enabledInstances; 399 400 users.groups.mautrix-meta = { }; 401 users.groups.mautrix-meta-registration = { 402 members = lib.lists.optional config.services.matrix-synapse.enable "matrix-synapse"; 403 }; 404 405 services.matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) ( 406 let 407 registrationFiles = lib.attrValues ( 408 lib.mapAttrs (name: cfg: cfg.registrationFile) registerToSynapseInstances 409 ); 410 in 411 { 412 settings.app_service_config_files = registrationFiles; 413 } 414 ); 415 416 systemd.services = lib.mkMerge [ 417 { 418 matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) ( 419 let 420 registrationServices = lib.attrValues ( 421 lib.mapAttrs (name: cfg: cfg.registrationServiceUnit) registerToSynapseInstances 422 ); 423 in 424 { 425 wants = registrationServices; 426 after = registrationServices; 427 } 428 ); 429 } 430 431 (lib.mapAttrs' ( 432 name: cfg: 433 lib.nameValuePair "${metaName name}-registration" { 434 description = "Mautrix-Meta registration generation service - ${metaName name}"; 435 436 path = [ 437 pkgs.yq 438 pkgs.envsubst 439 upperCfg.package 440 ]; 441 442 script = '' 443 # substitute the settings file by environment variables 444 # in this case read from EnvironmentFile 445 rm -f '${settingsFile cfg}' 446 old_umask=$(umask) 447 umask 0177 448 envsubst \ 449 -o '${settingsFile cfg}' \ 450 -i '${settingsFileUnsubstituted cfg}' 451 452 config_has_tokens=$(yq '.appservice | has("as_token") and has("hs_token")' '${settingsFile cfg}') 453 registration_already_exists=$([[ -f '${cfg.registrationFile}' ]] && echo "true" || echo "false") 454 455 echo "There are tokens in the config: $config_has_tokens" 456 echo "Registration already existed: $registration_already_exists" 457 458 # tokens not configured from config/environment file, and registration file 459 # is already generated, override tokens in config to make sure they are not lost 460 if [[ $config_has_tokens == "false" && $registration_already_exists == "true" ]]; then 461 echo "Copying as_token, hs_token from registration into configuration" 462 yq -sY '.[0].appservice.as_token = .[1].as_token 463 | .[0].appservice.hs_token = .[1].hs_token 464 | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \ 465 > '${settingsFile cfg}.tmp' 466 mv '${settingsFile cfg}.tmp' '${settingsFile cfg}' 467 fi 468 469 # make sure --generate-registration does not affect config.yaml 470 cp '${settingsFile cfg}' '${settingsFile cfg}.tmp' 471 472 echo "Generating registration file" 473 mautrix-meta \ 474 --generate-registration \ 475 --config='${settingsFile cfg}.tmp' \ 476 --registration='${cfg.registrationFile}' 477 478 rm '${settingsFile cfg}.tmp' 479 480 # no tokens configured, and new were just generated by generate registration for first time 481 if [[ $config_has_tokens == "false" && $registration_already_exists == "false" ]]; then 482 echo "Copying newly generated as_token, hs_token from registration into configuration" 483 yq -sY '.[0].appservice.as_token = .[1].as_token 484 | .[0].appservice.hs_token = .[1].hs_token 485 | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \ 486 > '${settingsFile cfg}.tmp' 487 mv '${settingsFile cfg}.tmp' '${settingsFile cfg}' 488 fi 489 490 # Make sure correct tokens are in the registration file 491 if [[ $config_has_tokens == "true" || $registration_already_exists == "true" ]]; then 492 echo "Copying as_token, hs_token from configuration to the registration file" 493 yq -sY '.[1].as_token = .[0].appservice.as_token 494 | .[1].hs_token = .[0].appservice.hs_token 495 | .[1]' '${settingsFile cfg}' '${cfg.registrationFile}' \ 496 > '${cfg.registrationFile}.tmp' 497 mv '${cfg.registrationFile}.tmp' '${cfg.registrationFile}' 498 fi 499 500 umask $old_umask 501 502 chown :mautrix-meta-registration '${cfg.registrationFile}' 503 chmod 640 '${cfg.registrationFile}' 504 ''; 505 506 serviceConfig = { 507 Type = "oneshot"; 508 UMask = 27; 509 510 User = "mautrix-meta-${name}"; 511 Group = "mautrix-meta"; 512 513 SystemCallFilter = [ "@system-service" ]; 514 515 ProtectSystem = "strict"; 516 ProtectHome = true; 517 518 ReadWritePaths = fullDataDir cfg; 519 StateDirectory = cfg.dataDir; 520 EnvironmentFile = cfg.environmentFile; 521 }; 522 523 restartTriggers = [ (settingsFileUnsubstituted cfg) ]; 524 } 525 ) enabledInstances) 526 527 (lib.mapAttrs' ( 528 name: cfg: 529 lib.nameValuePair "${metaName name}" { 530 description = "Mautrix-Meta bridge - ${metaName name}"; 531 wantedBy = [ "multi-user.target" ]; 532 wants = [ "network-online.target" ] ++ cfg.serviceDependencies; 533 after = [ "network-online.target" ] ++ cfg.serviceDependencies; 534 535 serviceConfig = { 536 Type = "simple"; 537 538 User = "mautrix-meta-${name}"; 539 Group = "mautrix-meta"; 540 PrivateUsers = true; 541 542 LockPersonality = true; 543 MemoryDenyWriteExecute = true; 544 NoNewPrivileges = true; 545 PrivateDevices = true; 546 PrivateTmp = true; 547 ProtectClock = true; 548 ProtectControlGroups = true; 549 ProtectHome = true; 550 ProtectHostname = true; 551 ProtectKernelLogs = true; 552 ProtectKernelModules = true; 553 ProtectKernelTunables = true; 554 ProtectSystem = "strict"; 555 Restart = "on-failure"; 556 RestartSec = "30s"; 557 RestrictRealtime = true; 558 RestrictSUIDSGID = true; 559 SystemCallArchitectures = "native"; 560 SystemCallErrorNumber = "EPERM"; 561 SystemCallFilter = [ "@system-service" ]; 562 UMask = 27; 563 564 WorkingDirectory = fullDataDir cfg; 565 ReadWritePaths = fullDataDir cfg; 566 StateDirectory = cfg.dataDir; 567 EnvironmentFile = cfg.environmentFile; 568 569 ExecStart = lib.escapeShellArgs [ 570 (lib.getExe upperCfg.package) 571 "--config=${settingsFile cfg}" 572 ]; 573 }; 574 restartTriggers = [ (settingsFileUnsubstituted cfg) ]; 575 } 576 ) enabledInstances) 577 ]; 578 }) 579 { 580 services.mautrix-meta.instances = 581 let 582 inherit (lib.modules) mkDefault; 583 in 584 { 585 instagram = { 586 settings = { 587 network.mode = mkDefault "instagram"; 588 589 appservice = { 590 id = mkDefault "instagram"; 591 port = mkDefault 29320; 592 bot = { 593 username = mkDefault "instagrambot"; 594 displayname = mkDefault "Instagram bridge bot"; 595 avatar = mkDefault "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv"; 596 }; 597 username_template = mkDefault "instagram_{{.}}"; 598 }; 599 }; 600 }; 601 facebook = { 602 settings = { 603 network.mode = mkDefault "facebook"; 604 605 appservice = { 606 id = mkDefault "facebook"; 607 port = mkDefault 29321; 608 bot = { 609 username = mkDefault "facebookbot"; 610 displayname = mkDefault "Facebook bridge bot"; 611 avatar = mkDefault "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak"; 612 }; 613 username_template = mkDefault "facebook_{{.}}"; 614 }; 615 }; 616 }; 617 }; 618 } 619 ]; 620 621 meta.maintainers = with lib.maintainers; [ ]; 622}