about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/matrix/mautrix-meta.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/modules/services/matrix/mautrix-meta.nix')
-rw-r--r--nixpkgs/nixos/modules/services/matrix/mautrix-meta.nix562
1 files changed, 562 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/matrix/mautrix-meta.nix b/nixpkgs/nixos/modules/services/matrix/mautrix-meta.nix
new file mode 100644
index 000000000000..f0905c3af129
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/matrix/mautrix-meta.nix
@@ -0,0 +1,562 @@
+{ 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 ];
+}