at master 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 ] 231 ++ (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) 232 ++ (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") 233 ++ (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service"); 234 235 defaultText = '' 236 [ config.registrationServiceUnit ] ++ 237 (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) ++ 238 (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") ++ 239 (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service"); 240 ''; 241 description = '' 242 List of Systemd services to require and wait for when starting the application service. 243 ''; 244 }; 245 246 serviceUnit = lib.mkOption { 247 type = lib.types.str; 248 readOnly = true; 249 description = '' 250 The systemd unit (a service or a target) for other services to depend on if they 251 need to be started after matrix-synapse. 252 253 This option is useful as the actual parent unit for all matrix-synapse processes 254 changes when configuring workers. 255 ''; 256 }; 257 258 registrationServiceUnit = lib.mkOption { 259 type = lib.types.str; 260 readOnly = true; 261 description = '' 262 The registration service that generates the registration file. 263 264 Systemd unit (a service or a target) for other services to depend on if they 265 need to be started after mautrix-meta registration service. 266 267 This option is useful as the actual parent unit for all matrix-synapse processes 268 changes when configuring workers. 269 ''; 270 }; 271 }; 272 273 config = { 274 serviceUnit = (metaName name) + ".service"; 275 registrationServiceUnit = (metaName name) + "-registration.service"; 276 registrationFile = (fullDataDir config) + "/meta-registration.yaml"; 277 }; 278 } 279 ) 280 ); 281 282 description = '' 283 Configuration of multiple `mautrix-meta` instances. 284 `services.mautrix-meta.instances.facebook` and `services.mautrix-meta.instances.instagram` 285 come preconfigured with network.mode, appservice.id, bot username, display name and avatar. 286 ''; 287 288 example = '' 289 { 290 facebook = { 291 enable = true; 292 settings = { 293 homeserver.domain = "example.com"; 294 }; 295 }; 296 297 instagram = { 298 enable = true; 299 settings = { 300 homeserver.domain = "example.com"; 301 }; 302 }; 303 304 messenger = { 305 enable = true; 306 settings = { 307 network.mode = "messenger"; 308 homeserver.domain = "example.com"; 309 appservice = { 310 id = "messenger"; 311 bot = { 312 username = "messengerbot"; 313 displayname = "Messenger bridge bot"; 314 avatar = "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak"; 315 }; 316 }; 317 }; 318 }; 319 } 320 ''; 321 }; 322 }; 323 }; 324 325 config = lib.mkMerge [ 326 (lib.mkIf (enabledInstances != { }) { 327 assertions = lib.mkMerge ( 328 lib.attrValues ( 329 lib.mapAttrs (name: cfg: [ 330 { 331 assertion = cfg.settings.homeserver.domain != "" && cfg.settings.homeserver.address != ""; 332 message = '' 333 The options with information about the homeserver: 334 `services.mautrix-meta.instances.${name}.settings.homeserver.domain` and 335 `services.mautrix-meta.instances.${name}.settings.homeserver.address` have to be set. 336 ''; 337 } 338 { 339 assertion = builtins.elem cfg.settings.network.mode [ 340 "facebook" 341 "facebook-tor" 342 "messenger" 343 "instagram" 344 ]; 345 message = '' 346 The option `services.mautrix-meta.instances.${name}.settings.network.mode` has to be set 347 to one of: facebook, facebook-tor, messenger, instagram. 348 This configures the mode of the bridge. 349 ''; 350 } 351 { 352 assertion = cfg.settings.bridge.permissions != { }; 353 message = '' 354 The option `services.mautrix-meta.instances.${name}.settings.bridge.permissions` has to be set. 355 ''; 356 } 357 { 358 assertion = cfg.settings.appservice.id != ""; 359 message = '' 360 The option `services.mautrix-meta.instances.${name}.settings.appservice.id` has to be set. 361 ''; 362 } 363 { 364 assertion = cfg.settings.appservice.bot.username != ""; 365 message = '' 366 The option `services.mautrix-meta.instances.${name}.settings.appservice.bot.username` has to be set. 367 ''; 368 } 369 { 370 assertion = !(cfg.settings ? bridge.disable_xma); 371 message = '' 372 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. 373 ''; 374 } 375 { 376 assertion = !(cfg.settings ? bridge.displayname_template); 377 message = '' 378 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. 379 ''; 380 } 381 { 382 assertion = !(cfg.settings ? meta); 383 message = '' 384 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. 385 ''; 386 } 387 ]) enabledInstances 388 ) 389 ); 390 391 users.users = lib.mapAttrs' ( 392 name: cfg: 393 lib.nameValuePair "mautrix-meta-${name}" { 394 isSystemUser = true; 395 group = "mautrix-meta"; 396 extraGroups = [ "mautrix-meta-registration" ]; 397 description = "Mautrix-Meta-${name} bridge user"; 398 } 399 ) enabledInstances; 400 401 users.groups.mautrix-meta = { }; 402 users.groups.mautrix-meta-registration = { 403 members = lib.lists.optional config.services.matrix-synapse.enable "matrix-synapse"; 404 }; 405 406 services.matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) ( 407 let 408 registrationFiles = lib.attrValues ( 409 lib.mapAttrs (name: cfg: cfg.registrationFile) registerToSynapseInstances 410 ); 411 in 412 { 413 settings.app_service_config_files = registrationFiles; 414 } 415 ); 416 417 systemd.services = lib.mkMerge [ 418 { 419 matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) ( 420 let 421 registrationServices = lib.attrValues ( 422 lib.mapAttrs (name: cfg: cfg.registrationServiceUnit) registerToSynapseInstances 423 ); 424 in 425 { 426 wants = registrationServices; 427 after = registrationServices; 428 } 429 ); 430 } 431 432 (lib.mapAttrs' ( 433 name: cfg: 434 lib.nameValuePair "${metaName name}-registration" { 435 description = "Mautrix-Meta registration generation service - ${metaName name}"; 436 437 path = [ 438 pkgs.yq 439 pkgs.envsubst 440 upperCfg.package 441 ]; 442 443 script = '' 444 # substitute the settings file by environment variables 445 # in this case read from EnvironmentFile 446 rm -f '${settingsFile cfg}' 447 old_umask=$(umask) 448 umask 0177 449 envsubst \ 450 -o '${settingsFile cfg}' \ 451 -i '${settingsFileUnsubstituted cfg}' 452 453 config_has_tokens=$(yq '.appservice | has("as_token") and has("hs_token")' '${settingsFile cfg}') 454 registration_already_exists=$([[ -f '${cfg.registrationFile}' ]] && echo "true" || echo "false") 455 456 echo "There are tokens in the config: $config_has_tokens" 457 echo "Registration already existed: $registration_already_exists" 458 459 # tokens not configured from config/environment file, and registration file 460 # is already generated, override tokens in config to make sure they are not lost 461 if [[ $config_has_tokens == "false" && $registration_already_exists == "true" ]]; then 462 echo "Copying as_token, hs_token from registration into configuration" 463 yq -sY '.[0].appservice.as_token = .[1].as_token 464 | .[0].appservice.hs_token = .[1].hs_token 465 | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \ 466 > '${settingsFile cfg}.tmp' 467 mv '${settingsFile cfg}.tmp' '${settingsFile cfg}' 468 fi 469 470 # make sure --generate-registration does not affect config.yaml 471 cp '${settingsFile cfg}' '${settingsFile cfg}.tmp' 472 473 echo "Generating registration file" 474 mautrix-meta \ 475 --generate-registration \ 476 --config='${settingsFile cfg}.tmp' \ 477 --registration='${cfg.registrationFile}' 478 479 rm '${settingsFile cfg}.tmp' 480 481 # no tokens configured, and new were just generated by generate registration for first time 482 if [[ $config_has_tokens == "false" && $registration_already_exists == "false" ]]; then 483 echo "Copying newly generated as_token, hs_token from registration into configuration" 484 yq -sY '.[0].appservice.as_token = .[1].as_token 485 | .[0].appservice.hs_token = .[1].hs_token 486 | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \ 487 > '${settingsFile cfg}.tmp' 488 mv '${settingsFile cfg}.tmp' '${settingsFile cfg}' 489 fi 490 491 # Make sure correct tokens are in the registration file 492 if [[ $config_has_tokens == "true" || $registration_already_exists == "true" ]]; then 493 echo "Copying as_token, hs_token from configuration to the registration file" 494 yq -sY '.[1].as_token = .[0].appservice.as_token 495 | .[1].hs_token = .[0].appservice.hs_token 496 | .[1]' '${settingsFile cfg}' '${cfg.registrationFile}' \ 497 > '${cfg.registrationFile}.tmp' 498 mv '${cfg.registrationFile}.tmp' '${cfg.registrationFile}' 499 fi 500 501 umask $old_umask 502 503 chown :mautrix-meta-registration '${cfg.registrationFile}' 504 chmod 640 '${cfg.registrationFile}' 505 ''; 506 507 serviceConfig = { 508 Type = "oneshot"; 509 UMask = 27; 510 511 User = "mautrix-meta-${name}"; 512 Group = "mautrix-meta"; 513 514 SystemCallFilter = [ "@system-service" ]; 515 516 ProtectSystem = "strict"; 517 ProtectHome = true; 518 519 ReadWritePaths = fullDataDir cfg; 520 StateDirectory = cfg.dataDir; 521 EnvironmentFile = cfg.environmentFile; 522 }; 523 524 restartTriggers = [ (settingsFileUnsubstituted cfg) ]; 525 } 526 ) enabledInstances) 527 528 (lib.mapAttrs' ( 529 name: cfg: 530 lib.nameValuePair "${metaName name}" { 531 description = "Mautrix-Meta bridge - ${metaName name}"; 532 wantedBy = [ "multi-user.target" ]; 533 wants = [ "network-online.target" ] ++ cfg.serviceDependencies; 534 after = [ "network-online.target" ] ++ cfg.serviceDependencies; 535 536 serviceConfig = { 537 Type = "simple"; 538 539 User = "mautrix-meta-${name}"; 540 Group = "mautrix-meta"; 541 PrivateUsers = true; 542 543 LockPersonality = true; 544 MemoryDenyWriteExecute = true; 545 NoNewPrivileges = true; 546 PrivateDevices = true; 547 PrivateTmp = true; 548 ProtectClock = true; 549 ProtectControlGroups = true; 550 ProtectHome = true; 551 ProtectHostname = true; 552 ProtectKernelLogs = true; 553 ProtectKernelModules = true; 554 ProtectKernelTunables = true; 555 ProtectSystem = "strict"; 556 Restart = "on-failure"; 557 RestartSec = "30s"; 558 RestrictRealtime = true; 559 RestrictSUIDSGID = true; 560 SystemCallArchitectures = "native"; 561 SystemCallErrorNumber = "EPERM"; 562 SystemCallFilter = [ "@system-service" ]; 563 UMask = 27; 564 565 WorkingDirectory = fullDataDir cfg; 566 ReadWritePaths = fullDataDir cfg; 567 StateDirectory = cfg.dataDir; 568 EnvironmentFile = cfg.environmentFile; 569 570 ExecStart = lib.escapeShellArgs [ 571 (lib.getExe upperCfg.package) 572 "--config=${settingsFile cfg}" 573 ]; 574 }; 575 restartTriggers = [ (settingsFileUnsubstituted cfg) ]; 576 } 577 ) enabledInstances) 578 ]; 579 }) 580 { 581 services.mautrix-meta.instances = 582 let 583 inherit (lib.modules) mkDefault; 584 in 585 { 586 instagram = { 587 settings = { 588 network.mode = mkDefault "instagram"; 589 590 appservice = { 591 id = mkDefault "instagram"; 592 port = mkDefault 29320; 593 bot = { 594 username = mkDefault "instagrambot"; 595 displayname = mkDefault "Instagram bridge bot"; 596 avatar = mkDefault "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv"; 597 }; 598 username_template = mkDefault "instagram_{{.}}"; 599 }; 600 }; 601 }; 602 facebook = { 603 settings = { 604 network.mode = mkDefault "facebook"; 605 606 appservice = { 607 id = mkDefault "facebook"; 608 port = mkDefault 29321; 609 bot = { 610 username = mkDefault "facebookbot"; 611 displayname = mkDefault "Facebook bridge bot"; 612 avatar = mkDefault "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak"; 613 }; 614 username_template = mkDefault "facebook_{{.}}"; 615 }; 616 }; 617 }; 618 }; 619 } 620 ]; 621 622 meta.maintainers = with lib.maintainers; [ ]; 623}