summaryrefslogtreecommitdiffstats
path: root/nixos/modules/services/matrix
diff options
context:
space:
mode:
authorKevin Cox <kevincox@kevincox.ca>2024-04-04 18:07:19 -0400
committerGitHub <noreply@github.com>2024-04-04 18:07:19 -0400
commit9c636e8416a9877e5c46e5ad9d8a05b425df1d88 (patch)
treeb07e02465d86f1cfe2f9c26e1835601e089ec82d /nixos/modules/services/matrix
parentef0fcb6ee8fb485785d3a193d2039c8a4a73d4f9 (diff)
parent5effc7956aca6dafc8ac3d4e6378b36ab833aa25 (diff)
Merge pull request #296718 from Rutherther/mautrix-meta-init
mautrix-meta: init at 0.2.0
Diffstat (limited to 'nixos/modules/services/matrix')
-rw-r--r--nixos/modules/services/matrix/mautrix-meta.nix562
1 files changed, 562 insertions, 0 deletions
diff --git a/nixos/modules/services/matrix/mautrix-meta.nix b/nixos/modules/services/matrix/mautrix-meta.nix
new file mode 100644
index 000000000000..b8a5cdc72065
--- /dev/null
+++ b/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 ];
+}