{ config, pkgs, lib, ... }: let settingsFormat = pkgs.formats.yaml {}; upperConfig = config; cfg = config.services.mautrix-meta; upperCfg = cfg; fullDataDir = cfg: "/var/lib/${cfg.dataDir}"; settingsFile = cfg: "${fullDataDir cfg}/config.yaml"; settingsFileUnsubstituted = cfg: settingsFormat.generate "mautrix-meta-config.yaml" cfg.settings; metaName = name: "mautrix-meta-${name}"; enabledInstances = lib.filterAttrs (name: config: config.enable) config.services.mautrix-meta.instances; registerToSynapseInstances = lib.filterAttrs (name: config: config.enable && config.registerToSynapse) config.services.mautrix-meta.instances; in { options = { services.mautrix-meta = { package = lib.mkPackageOption pkgs "mautrix-meta" { }; instances = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule ({ config, name, ... }: { options = { enable = lib.mkEnableOption "Mautrix-Meta, a Matrix <-> Facebook and Matrix <-> Instagram hybrid puppeting/relaybot bridge"; dataDir = lib.mkOption { type = lib.types.str; default = metaName name; description = '' Path to the directory with database, registration, and other data for the bridge service. This path is relative to `/var/lib`, it cannot start with `../` (it cannot be outside of `/var/lib`). ''; }; registrationFile = lib.mkOption { type = lib.types.path; readOnly = true; description = '' Path to the yaml registration file of the appservice. ''; }; registerToSynapse = lib.mkOption { type = lib.types.bool; default = true; description = '' Whether to add registration file to `services.matrix-synapse.settings.app_service_config_files` and make Synapse wait for registration service. ''; }; settings = lib.mkOption rec { apply = lib.recursiveUpdate default; inherit (settingsFormat) type; default = { homeserver = { software = "standard"; domain = ""; address = ""; }; appservice = { id = ""; database = { type = "sqlite3-fk-wal"; uri = "file:${fullDataDir config}/mautrix-meta.db?_txlock=immediate"; }; bot = { username = ""; }; hostname = "localhost"; port = 29319; address = "http://${config.settings.appservice.hostname}:${toString config.settings.appservice.port}"; }; meta = { mode = ""; }; bridge = { # Enable encryption by default to make the bridge more secure encryption = { allow = true; default = true; require = true; # Recommended options from mautrix documentation # for additional security. delete_keys = { dont_store_outbound = true; ratchet_on_decrypt = true; delete_fully_used_on_decrypt = true; delete_prev_on_new_session = true; delete_on_device_delete = true; periodically_delete_expired = true; delete_outdated_inbound = true; }; verification_levels = { receive = "cross-signed-tofu"; send = "cross-signed-tofu"; share = "cross-signed-tofu"; }; }; permissions = {}; }; logging = { min_level = "info"; writers = lib.singleton { type = "stdout"; format = "pretty-colored"; time_format = " "; }; }; }; defaultText = '' { homeserver = { software = "standard"; address = "https://''${config.settings.homeserver.domain}"; }; appservice = { database = { type = "sqlite3-fk-wal"; uri = "file:''${fullDataDir config}/mautrix-meta.db?_txlock=immediate"; }; hostname = "localhost"; port = 29319; address = "http://''${config.settings.appservice.hostname}:''${toString config.settings.appservice.port}"; }; bridge = { # Require encryption by default to make the bridge more secure encryption = { allow = true; default = true; require = true; # Recommended options from mautrix documentation # for optimal security. delete_keys = { dont_store_outbound = true; ratchet_on_decrypt = true; delete_fully_used_on_decrypt = true; delete_prev_on_new_session = true; delete_on_device_delete = true; periodically_delete_expired = true; delete_outdated_inbound = true; }; verification_levels = { receive = "cross-signed-tofu"; send = "cross-signed-tofu"; share = "cross-signed-tofu"; }; }; }; logging = { min_level = "info"; writers = lib.singleton { type = "stdout"; format = "pretty-colored"; time_format = " "; }; }; }; ''; description = '' {file}`config.yaml` configuration as a Nix attribute set. Configuration options should match those described in [example-config.yaml](https://github.com/mautrix/meta/blob/main/example-config.yaml). Secret tokens should be specified using {option}`environmentFile` instead ''; }; environmentFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' File containing environment variables to substitute when copying the configuration out of Nix store to the `services.mautrix-meta.dataDir`. Can be used for storing the secrets without making them available in the Nix store. For example, you can set `services.mautrix-meta.settings.appservice.as_token = "$MAUTRIX_META_APPSERVICE_AS_TOKEN"` and then specify `MAUTRIX_META_APPSERVICE_AS_TOKEN="{token}"` in the environment file. This value will get substituted into the configuration file as as token. ''; }; serviceDependencies = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ config.registrationServiceUnit ] ++ (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) ++ (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") ++ (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service"); defaultText = '' [ config.registrationServiceUnit ] ++ (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) ++ (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") ++ (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service"); ''; description = '' List of Systemd services to require and wait for when starting the application service. ''; }; serviceUnit = lib.mkOption { type = lib.types.str; readOnly = true; description = '' The systemd unit (a service or a target) for other services to depend on if they need to be started after matrix-synapse. This option is useful as the actual parent unit for all matrix-synapse processes changes when configuring workers. ''; }; registrationServiceUnit = lib.mkOption { type = lib.types.str; readOnly = true; description = '' The registration service that generates the registration file. Systemd unit (a service or a target) for other services to depend on if they need to be started after mautrix-meta registration service. This option is useful as the actual parent unit for all matrix-synapse processes changes when configuring workers. ''; }; }; config = { serviceUnit = (metaName name) + ".service"; registrationServiceUnit = (metaName name) + "-registration.service"; registrationFile = (fullDataDir config) + "/meta-registration.yaml"; }; })); description = '' Configuration of multiple `mautrix-meta` instances. `services.mautrix-meta.instances.facebook` and `services.mautrix-meta.instances.instagram` come preconfigured with meta.mode, appservice.id, bot username, display name and avatar. ''; example = '' { facebook = { enable = true; settings = { homeserver.domain = "example.com"; }; }; instagram = { enable = true; settings = { homeserver.domain = "example.com"; }; }; messenger = { enable = true; settings = { meta.mode = "messenger"; homeserver.domain = "example.com"; appservice = { id = "messenger"; bot = { username = "messengerbot"; displayname = "Messenger bridge bot"; avatar = "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak"; }; }; }; }; } ''; }; }; }; config = lib.mkMerge [ (lib.mkIf (enabledInstances != {}) { assertions = lib.mkMerge (lib.attrValues (lib.mapAttrs (name: cfg: [ { assertion = cfg.settings.homeserver.domain != "" && cfg.settings.homeserver.address != ""; message = '' The options with information about the homeserver: `services.mautrix-meta.instances.${name}.settings.homeserver.domain` and `services.mautrix-meta.instances.${name}.settings.homeserver.address` have to be set. ''; } { assertion = builtins.elem cfg.settings.meta.mode [ "facebook" "facebook-tor" "messenger" "instagram" ]; message = '' The option `services.mautrix-meta.instances.${name}.settings.meta.mode` has to be set to one of: facebook, facebook-tor, messenger, instagram. This configures the mode of the bridge. ''; } { assertion = cfg.settings.bridge.permissions != {}; message = '' The option `services.mautrix-meta.instances.${name}.settings.bridge.permissions` has to be set. ''; } { assertion = cfg.settings.appservice.id != ""; message = '' The option `services.mautrix-meta.instances.${name}.settings.appservice.id` has to be set. ''; } { assertion = cfg.settings.appservice.bot.username != ""; message = '' The option `services.mautrix-meta.instances.${name}.settings.appservice.bot.username` has to be set. ''; } ]) enabledInstances)); users.users = lib.mapAttrs' (name: cfg: lib.nameValuePair "mautrix-meta-${name}" { isSystemUser = true; group = "mautrix-meta"; extraGroups = [ "mautrix-meta-registration" ]; description = "Mautrix-Meta-${name} bridge user"; }) enabledInstances; users.groups.mautrix-meta = {}; users.groups.mautrix-meta-registration = { members = lib.lists.optional config.services.matrix-synapse.enable "matrix-synapse"; }; services.matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) (let registrationFiles = lib.attrValues (lib.mapAttrs (name: cfg: cfg.registrationFile) registerToSynapseInstances); in { settings.app_service_config_files = registrationFiles; }); systemd.services = lib.mkMerge [ { matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) (let registrationServices = lib.attrValues (lib.mapAttrs (name: cfg: cfg.registrationServiceUnit) registerToSynapseInstances); in { wants = registrationServices; after = registrationServices; }); } (lib.mapAttrs' (name: cfg: lib.nameValuePair "${metaName name}-registration" { description = "Mautrix-Meta registration generation service - ${metaName name}"; path = [ pkgs.yq pkgs.envsubst upperCfg.package ]; script = '' # substitute the settings file by environment variables # in this case read from EnvironmentFile rm -f '${settingsFile cfg}' old_umask=$(umask) umask 0177 envsubst \ -o '${settingsFile cfg}' \ -i '${settingsFileUnsubstituted cfg}' config_has_tokens=$(yq '.appservice | has("as_token") and has("hs_token")' '${settingsFile cfg}') registration_already_exists=$([[ -f '${cfg.registrationFile}' ]] && echo "true" || echo "false") echo "There are tokens in the config: $config_has_tokens" echo "Registration already existed: $registration_already_exists" # tokens not configured from config/environment file, and registration file # is already generated, override tokens in config to make sure they are not lost if [[ $config_has_tokens == "false" && $registration_already_exists == "true" ]]; then echo "Copying as_token, hs_token from registration into configuration" yq -sY '.[0].appservice.as_token = .[1].as_token | .[0].appservice.hs_token = .[1].hs_token | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \ > '${settingsFile cfg}.tmp' mv '${settingsFile cfg}.tmp' '${settingsFile cfg}' fi # make sure --generate-registration does not affect config.yaml cp '${settingsFile cfg}' '${settingsFile cfg}.tmp' echo "Generating registration file" mautrix-meta \ --generate-registration \ --config='${settingsFile cfg}.tmp' \ --registration='${cfg.registrationFile}' rm '${settingsFile cfg}.tmp' # no tokens configured, and new were just generated by generate registration for first time if [[ $config_has_tokens == "false" && $registration_already_exists == "false" ]]; then echo "Copying newly generated as_token, hs_token from registration into configuration" yq -sY '.[0].appservice.as_token = .[1].as_token | .[0].appservice.hs_token = .[1].hs_token | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \ > '${settingsFile cfg}.tmp' mv '${settingsFile cfg}.tmp' '${settingsFile cfg}' fi # Make sure correct tokens are in the registration file if [[ $config_has_tokens == "true" || $registration_already_exists == "true" ]]; then echo "Copying as_token, hs_token from configuration to the registration file" yq -sY '.[1].as_token = .[0].appservice.as_token | .[1].hs_token = .[0].appservice.hs_token | .[1]' '${settingsFile cfg}' '${cfg.registrationFile}' \ > '${cfg.registrationFile}.tmp' mv '${cfg.registrationFile}.tmp' '${cfg.registrationFile}' fi umask $old_umask chown :mautrix-meta-registration '${cfg.registrationFile}' chmod 640 '${cfg.registrationFile}' ''; serviceConfig = { Type = "oneshot"; UMask = 0027; User = "mautrix-meta-${name}"; Group = "mautrix-meta"; SystemCallFilter = [ "@system-service" ]; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = fullDataDir cfg; StateDirectory = cfg.dataDir; EnvironmentFile = cfg.environmentFile; }; restartTriggers = [ (settingsFileUnsubstituted cfg) ]; }) enabledInstances) (lib.mapAttrs' (name: cfg: lib.nameValuePair "${metaName name}" { description = "Mautrix-Meta bridge - ${metaName name}"; wantedBy = [ "multi-user.target" ]; wants = [ "network-online.target" ] ++ cfg.serviceDependencies; after = [ "network-online.target" ] ++ cfg.serviceDependencies; serviceConfig = { Type = "simple"; User = "mautrix-meta-${name}"; Group = "mautrix-meta"; PrivateUsers = true; LockPersonality = true; MemoryDenyWriteExecute = true; NoNewPrivileges = true; PrivateDevices = true; PrivateTmp = true; ProtectClock = true; ProtectControlGroups = true; ProtectHome = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectSystem = "strict"; Restart = "on-failure"; RestartSec = "30s"; RestrictRealtime = true; RestrictSUIDSGID = true; SystemCallArchitectures = "native"; SystemCallErrorNumber = "EPERM"; SystemCallFilter = ["@system-service"]; UMask = 0027; WorkingDirectory = fullDataDir cfg; ReadWritePaths = fullDataDir cfg; StateDirectory = cfg.dataDir; EnvironmentFile = cfg.environmentFile; ExecStart = lib.escapeShellArgs [ (lib.getExe upperCfg.package) "--config=${settingsFile cfg}" ]; }; restartTriggers = [ (settingsFileUnsubstituted cfg) ]; }) enabledInstances) ]; }) { services.mautrix-meta.instances = let inherit (lib.modules) mkDefault; in { instagram = { settings = { meta.mode = mkDefault "instagram"; bridge = { username_template = mkDefault "instagram_{{.}}"; }; appservice = { id = mkDefault "instagram"; port = mkDefault 29320; bot = { username = mkDefault "instagrambot"; displayname = mkDefault "Instagram bridge bot"; avatar = mkDefault "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv"; }; }; }; }; facebook = { settings = { meta.mode = mkDefault "facebook"; bridge = { username_template = mkDefault "facebook_{{.}}"; }; appservice = { id = mkDefault "facebook"; port = mkDefault 29321; bot = { username = mkDefault "facebookbot"; displayname = mkDefault "Facebook bridge bot"; avatar = mkDefault "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak"; }; }; }; }; }; } ]; meta.maintainers = with lib.maintainers; [ rutherther ]; }