diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 1a86beaba73f754..20abba479a08125 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -91,6 +91,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable). The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been marked deprecated and will be dropped after 24.05 due to lack of maintenance of the anki-sync-server softwares. +- [mautrix-meta](https://github.com/mautrix/meta), a Matrix <-> Facebook and Matrix <-> Instagram hybrid puppeting/relaybot bridge. Available as services.mautrix-meta + - [transfer-sh](https://github.com/dutchcoders/transfer.sh), a tool that supports easy and fast file sharing from the command-line. Available as [services.transfer-sh](#opt-services.transfer-sh.enable). - [Suwayomi Server](https://github.com/Suwayomi/Suwayomi-Server), a free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org). Available as [services.suwayomi-server](#opt-services.suwayomi-server.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 2ccaea466c6a756..e4977b527f57962 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -648,6 +648,7 @@ ./services/matrix/hebbot.nix ./services/matrix/maubot.nix ./services/matrix/mautrix-facebook.nix + ./services/matrix/mautrix-meta.nix ./services/matrix/mautrix-telegram.nix ./services/matrix/mautrix-whatsapp.nix ./services/matrix/mjolnir.nix diff --git a/nixos/modules/services/matrix/mautrix-meta.nix b/nixos/modules/services/matrix/mautrix-meta.nix new file mode 100644 index 000000000000000..afe069fd9144365 --- /dev/null +++ b/nixos/modules/services/matrix/mautrix-meta.nix @@ -0,0 +1,489 @@ +{ 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; +in { + options = { + services.mautrix-meta = { + + package = lib.mkPackageOption pkgs "mautrix-meta" { }; + + instances = lib.mkOption rec { + type = lib.types.attrsOf (lib.types.submodule ({ config, name, ... }: let + in { + + options = { + + enable = lib.mkEnableOption (lib.mdDoc "Mautrix-Meta, a Matrix <-> Facebook and Matrix <-> Instagram hybrid puppeting/relaybot bridge"); + + dataDir = lib.mkOption { + type = lib.types.str; + default = metaName name; + description = lib.mdDoc '' + 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 = lib.mdDoc '' + Path to the yaml registration file of the appservice. + ''; + }; + + settings = lib.mkOption rec { + apply = lib.recursiveUpdate default; + inherit (settingsFormat) type; + default = { + homeserver = { + software = "standard"; + + domain = ""; + address = ""; + }; + + 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}"; + }; + + 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 = lib.mdDoc '' + {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 = lib.mdDoc '' + 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 = lib.mdDoc '' + List of Systemd services to require and wait for when starting the application service. + ''; + }; + + serviceUnit = lib.mkOption { + type = lib.types.str; + readOnly = true; + description = lib.mdDoc '' + 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 = lib.mdDoc '' + 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 = lib.mdDoc '' + 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 = lib.mdDoc '' + { + 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.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. + ''; + }) enabledInstances)) ++ (lib.attrValues (lib.mapAttrs (name: cfg: { + 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. + ''; + }) enabledInstances)) ++ (lib.attrValues (lib.mapAttrs (name: cfg: { + assertion = cfg.settings.bridge.permissions != {}; + message = '' + The option with permission for the bridge: services.mautrix-meta.instances.${name}.settings.bridge.permissions has to be set. + ''; + }) enabledInstances)); + + users.users.mautrix-meta = { + isSystemUser = true; + group = "mautrix-meta"; + description = "Mautrix-Meta bridge user"; + }; + + users.groups.mautrix-meta = {}; + + systemd.services = lib.attrsets.concatMapAttrs (name: cfg: { + matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) { + wants = [ cfg.registrationServiceUnit ]; + after = [ cfg.registrationServiceUnit ]; + serviceConfig = { + # Allow synapse to read the registration file + ReadWritePaths = [ (fullDataDir cfg) ]; + }; + }; + + "${metaName name}-registration" = { + description = "Mautrix-Meta registration generation service - ${metaName name}"; + + script = '' + # substitute the settings file by environment variables + # in this case read from EnvironmentFile + test -f '${settingsFile cfg}' && rm -f '${settingsFile cfg}' + old_umask=$(umask) + umask 0177 + ${lib.getExe pkgs.envsubst} \ + -o '${settingsFile cfg}' \ + -i '${settingsFileUnsubstituted cfg}' + + umask 0127 + + has_tokens="$(${lib.getExe pkgs.yq} '.appservice | has("as_token") and has("hs_token")' '${settingsFileUnsubstituted cfg}')" + generated=0 + + if [[ (! -f '${cfg.registrationFile}') ]]; then + echo "Generating registration file" + ${lib.getExe upperCfg.package} \ + --generate-registration \ + --config='${settingsFile cfg}' \ + --registration='${cfg.registrationFile}' + generated=1 + fi + + umask 0177 + + # Make sure correct tokens are in the registration file + if [[ "$has_tokens" == "true" ]]; then + echo "Substituting as_token, hs_token inside the registration file" + ${lib.getExe pkgs.yq} -s '.[1].as_token = .[0].appservice.as_token + | .[1].hs_token = .[0].appservice.hs_token + | .[1]' '${settingsFileUnsubstituted cfg}' '${cfg.registrationFile}' \ + > '${cfg.registrationFile}.tmp' + mv '${cfg.registrationFile}.tmp' '${cfg.registrationFile}' + fi + + # Generation overrides as_token, hs_token. + if [[ "$generated" == "1" || "$has_tokens" == "false" ]]; then + echo "Substituting as_token, hs_token inside the config file" + ${lib.getExe pkgs.yq} -s '.[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 + + umask $old_umask + ''; #+ lib.optionalString config.services.matrix-synapse.enable '' + # chown mautrix-meta:matrix-synapse '${cfg.registrationFile}' + # ''; + + serviceConfig = { + UMask = 0027; + + User = "mautrix-meta"; + Group = "mautrix-meta"; + + SystemCallFilter = [ "@system-service" "@chown" ]; + CapabilityBoundingSet = [ "CAP_CHOWN" ]; + + ProtectSystem = "strict"; + ProtectHome = true; + + StateDirectory = cfg.dataDir; + ReadWritePaths = fullDataDir cfg; + }; + + restartTriggers = [ (settingsFileUnsubstituted cfg) ]; + }; + "${metaName name}" = { + description = "Mautrix-Meta bridge - ${metaName name}"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ] ++ cfg.serviceDependencies; + after = [ "network-online.target" ] ++ cfg.serviceDependencies; + + environment.HOME = fullDataDir cfg; + + serviceConfig = { + Type = "simple"; + + User = "mautrix-meta"; + 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.getExe upperCfg.package} --config='${settingsFile cfg}'"; + }; + restartTriggers = [ (settingsFileUnsubstituted cfg) ]; + }; + }) enabledInstances; + }) + { + services.mautrix-meta.instances = let + inherit (lib.modules) mkDefault; + in { + instagram = { + settings = { + meta.mode = "instagram"; + + appservice = { + id = mkDefault "instagram"; + port = 29320; + bot = { + username = mkDefault "instagrambot"; + displayname = mkDefault "Instagram bridge bot"; + avatar = mkDefault "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv"; + }; + }; + }; + }; + facebook = { + settings = { + meta.mode = mkDefault "facebook"; + + appservice = { + id = mkDefault "facebook"; + port = 29321; + bot = { + username = mkDefault "facebookbot"; + displayname = mkDefault "Facebook bridge bot"; + avatar = mkDefault "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak"; + }; + }; + }; + }; + }; + } + ]; + + meta.maintainers = with lib.maintainers; [ rutherther ]; +}