From b82d8dea03b7df0eb166110251286834d511d6b9 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sun, 17 Mar 2024 18:53:41 +0100 Subject: [PATCH 1/3] Add maintainer rutherther --- maintainers/maintainer-list.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index 07f5874226e4fe..62d06876989bbd 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -16909,6 +16909,12 @@ githubId = 2660; name = "Russell Sim"; }; + rutherther = { + name = "Rutherther"; + email = "rutherther@proton.me"; + github = "rutherther"; + githubId = 12197024; + }; ruuda = { email = "dev+nix@veniogames.com"; github = "ruuda"; From 404d79c3d068051478d0381ac0c0594540737705 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sun, 17 Mar 2024 18:55:50 +0100 Subject: [PATCH 2/3] mautrix-meta: init at v0.2.0 --- pkgs/by-name/ma/mautrix-meta/package.nix | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 pkgs/by-name/ma/mautrix-meta/package.nix diff --git a/pkgs/by-name/ma/mautrix-meta/package.nix b/pkgs/by-name/ma/mautrix-meta/package.nix new file mode 100644 index 00000000000000..198890a4b35abb --- /dev/null +++ b/pkgs/by-name/ma/mautrix-meta/package.nix @@ -0,0 +1,29 @@ +{ lib, buildGoModule, fetchFromGitHub, olm, config }: + +buildGoModule rec { + pname = "mautrix-meta"; + version = "0.2.0"; + + subPackages = [ "." ]; + + src = fetchFromGitHub { + owner = "mautrix"; + repo = "meta"; + rev = "v${version}"; + hash = "sha256-n0FpEHgnMdg6W5wahIT5HaF9AP/QYlLuUWJS+VrElgg="; + }; + + buildInputs = [ olm ]; + + vendorHash = "sha256-GkgIang3/1u0ybznHgK1l84bEiCj6u4qf8G+HgLGr90="; + + doCheck = false; + + meta = { + homepage = "https://github.com/mautrix/meta"; + description = "Matrix <-> Facebook and Mautrix <-> Instagram hybrid puppeting/relaybot bridge"; + license = lib.licenses.agpl3Plus; + maintainers = with lib.maintainers; [ rutherther ]; + mainProgram = "mautrix-meta"; + }; +} From 5effc7956aca6dafc8ac3d4e6378b36ab833aa25 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sun, 17 Mar 2024 18:56:18 +0100 Subject: [PATCH 3/3] mautrix-meta: init service Acked-by: Rutherther --- .../manual/release-notes/rl-2405.section.md | 2 + nixos/modules/module-list.nix | 1 + .../modules/services/matrix/mautrix-meta.nix | 562 ++++++++++++++++++ nixos/tests/all-tests.nix | 2 + nixos/tests/matrix/mautrix-meta-postgres.nix | 221 +++++++ nixos/tests/matrix/mautrix-meta-sqlite.nix | 247 ++++++++ 6 files changed, 1035 insertions(+) create mode 100644 nixos/modules/services/matrix/mautrix-meta.nix create mode 100644 nixos/tests/matrix/mautrix-meta-postgres.nix create mode 100644 nixos/tests/matrix/mautrix-meta-sqlite.nix diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 1a86beaba73f75..20abba479a0812 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 2ccaea466c6a75..e4977b527f5796 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 00000000000000..b8a5cdc72065bd --- /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 ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 89e92bc8a9998c..869b0e88a6db84 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -520,6 +520,8 @@ in { matrix-conduit = handleTest ./matrix/conduit.nix {}; matrix-synapse = handleTest ./matrix/synapse.nix {}; matrix-synapse-workers = handleTest ./matrix/synapse-workers.nix {}; + mautrix-meta-postgres = handleTest ./matrix/mautrix-meta-postgres.nix {}; + mautrix-meta-sqlite = handleTest ./matrix/mautrix-meta-sqlite.nix {}; mattermost = handleTest ./mattermost.nix {}; mealie = handleTest ./mealie.nix {}; mediamtx = handleTest ./mediamtx.nix {}; diff --git a/nixos/tests/matrix/mautrix-meta-postgres.nix b/nixos/tests/matrix/mautrix-meta-postgres.nix new file mode 100644 index 00000000000000..c9a45788afaf64 --- /dev/null +++ b/nixos/tests/matrix/mautrix-meta-postgres.nix @@ -0,0 +1,221 @@ +import ../make-test-python.nix ({ pkgs, ... }: + let + homeserverDomain = "server"; + homeserverUrl = "http://server:8008"; + userName = "alice"; + botUserName = "instagrambot"; + + asToken = "this-is-my-totally-randomly-generated-as-token"; + hsToken = "this-is-my-totally-randomly-generated-hs-token"; + in + { + name = "mautrix-meta-postgres"; + meta.maintainers = pkgs.mautrix-meta.meta.maintainers; + + nodes = { + server = { config, pkgs, ... }: { + services.postgresql = { + enable = true; + + ensureUsers = [ + { + name = "mautrix-meta-instagram"; + ensureDBOwnership = true; + } + ]; + + ensureDatabases = [ + "mautrix-meta-instagram" + ]; + }; + + systemd.services.mautrix-meta-instagram = { + wants = [ "postgres.service" ]; + after = [ "postgres.service" ]; + }; + + services.matrix-synapse = { + enable = true; + settings = { + database.name = "sqlite3"; + + enable_registration = true; + + # don't use this in production, always use some form of verification + enable_registration_without_verification = true; + + listeners = [ { + # The default but tls=false + bind_addresses = [ + "0.0.0.0" + ]; + port = 8008; + resources = [ { + "compress" = true; + "names" = [ "client" ]; + } { + "compress" = false; + "names" = [ "federation" ]; + } ]; + tls = false; + type = "http"; + } ]; + }; + }; + + services.mautrix-meta.instances.instagram = { + enable = true; + + environmentFile = pkgs.writeText ''my-secrets'' '' + AS_TOKEN=${asToken} + HS_TOKEN=${hsToken} + ''; + + settings = { + homeserver = { + address = homeserverUrl; + domain = homeserverDomain; + }; + + appservice = { + port = 8009; + + as_token = "$AS_TOKEN"; + hs_token = "$HS_TOKEN"; + + database = { + type = "postgres"; + uri = "postgres:///mautrix-meta-instagram?host=/var/run/postgresql"; + }; + + bot.username = botUserName; + }; + + bridge.permissions."@${userName}:server" = "user"; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8008 8009 ]; + }; + + client = { pkgs, ... }: { + environment.systemPackages = [ + (pkgs.writers.writePython3Bin "do_test" + { + libraries = [ pkgs.python3Packages.matrix-nio ]; + flakeIgnore = [ + # We don't live in the dark ages anymore. + # Languages like Python that are whitespace heavy will overrun + # 79 characters.. + "E501" + ]; + } '' + import sys + import functools + import asyncio + + from nio import AsyncClient, RoomMessageNotice, RoomCreateResponse, RoomInviteResponse + + + async def message_callback(matrix: AsyncClient, msg: str, _r, e): + print("Received matrix text message: ", e) + assert msg in e.body + exit(0) # Success! + + + async def run(homeserver: str): + matrix = AsyncClient(homeserver) + response = await matrix.register("${userName}", "foobar") + print("Matrix register response: ", response) + + # Open a DM with the bridge bot + response = await matrix.room_create() + print("Matrix create room response:", response) + assert isinstance(response, RoomCreateResponse) + room_id = response.room_id + + response = await matrix.room_invite(room_id, "@${botUserName}:${homeserverDomain}") + assert isinstance(response, RoomInviteResponse) + + callback = functools.partial( + message_callback, matrix, "Hello, I'm an Instagram bridge bot." + ) + matrix.add_event_callback(callback, RoomMessageNotice) + + print("Waiting for matrix message...") + await matrix.sync_forever(timeout=30000) + + + if __name__ == "__main__": + asyncio.run(run(sys.argv[1])) + '' + ) + ]; + }; + }; + + testScript = '' + def extract_token(data): + stdout = data[1] + stdout = stdout.strip() + line = stdout.split('\n')[-1] + return line.split(':')[-1].strip("\" '\n") + + def get_token_from(token, file): + data = server.execute(f"cat {file} | grep {token}") + return extract_token(data) + + def get_as_token_from(file): + return get_token_from("as_token", file) + + def get_hs_token_from(file): + return get_token_from("hs_token", file) + + config_yaml = "/var/lib/mautrix-meta-instagram/config.yaml" + registration_yaml = "/var/lib/mautrix-meta-instagram/meta-registration.yaml" + + expected_as_token = "${asToken}" + expected_hs_token = "${hsToken}" + + start_all() + + with subtest("start the server"): + # bridge + server.wait_for_unit("mautrix-meta-instagram.service") + + # homeserver + server.wait_for_unit("matrix-synapse.service") + + server.wait_for_open_port(8008) + # Bridge only opens the port after it contacts the homeserver + server.wait_for_open_port(8009) + + with subtest("ensure messages can be exchanged"): + client.succeed("do_test ${homeserverUrl} >&2") + + with subtest("ensure as_token, hs_token match from environment file"): + as_token = get_as_token_from(config_yaml) + hs_token = get_hs_token_from(config_yaml) + as_token_registration = get_as_token_from(registration_yaml) + hs_token_registration = get_hs_token_from(registration_yaml) + + assert as_token == expected_as_token, f"as_token in config should match the one specified (is: {as_token}, expected: {expected_as_token})" + assert hs_token == expected_hs_token, f"hs_token in config should match the one specified (is: {hs_token}, expected: {expected_hs_token})" + assert as_token_registration == expected_as_token, f"as_token in registration should match the one specified (is: {as_token_registration}, expected: {expected_as_token})" + assert hs_token_registration == expected_hs_token, f"hs_token in registration should match the one specified (is: {hs_token_registration}, expected: {expected_hs_token})" + + with subtest("ensure as_token and hs_token stays same after restart"): + server.systemctl("restart mautrix-meta-instagram") + server.wait_for_open_port(8009) + + as_token = get_as_token_from(config_yaml) + hs_token = get_hs_token_from(config_yaml) + as_token_registration = get_as_token_from(registration_yaml) + hs_token_registration = get_hs_token_from(registration_yaml) + + assert as_token == expected_as_token, f"as_token in config should match the one specified (is: {as_token}, expected: {expected_as_token})" + assert hs_token == expected_hs_token, f"hs_token in config should match the one specified (is: {hs_token}, expected: {expected_hs_token})" + assert as_token_registration == expected_as_token, f"as_token in registration should match the one specified (is: {as_token_registration}, expected: {expected_as_token})" + assert hs_token_registration == expected_hs_token, f"hs_token in registration should match the one specified (is: {hs_token_registration}, expected: {expected_hs_token})" + ''; + }) diff --git a/nixos/tests/matrix/mautrix-meta-sqlite.nix b/nixos/tests/matrix/mautrix-meta-sqlite.nix new file mode 100644 index 00000000000000..b5e580620049a1 --- /dev/null +++ b/nixos/tests/matrix/mautrix-meta-sqlite.nix @@ -0,0 +1,247 @@ +import ../make-test-python.nix ({ pkgs, ... }: + let + homeserverDomain = "server"; + homeserverUrl = "http://server:8008"; + username = "alice"; + instagramBotUsername = "instagrambot"; + facebookBotUsername = "facebookbot"; + in + { + name = "mautrix-meta-sqlite"; + meta.maintainers = pkgs.mautrix-meta.meta.maintainers; + + nodes = { + server = { config, pkgs, ... }: { + services.matrix-synapse = { + enable = true; + settings = { + database.name = "sqlite3"; + + enable_registration = true; + + # don't use this in production, always use some form of verification + enable_registration_without_verification = true; + + listeners = [ { + # The default but tls=false + bind_addresses = [ + "0.0.0.0" + ]; + port = 8008; + resources = [ { + "compress" = true; + "names" = [ "client" ]; + } { + "compress" = false; + "names" = [ "federation" ]; + } ]; + tls = false; + type = "http"; + } ]; + }; + }; + + services.mautrix-meta.instances.facebook = { + enable = true; + + settings = { + homeserver = { + address = homeserverUrl; + domain = homeserverDomain; + }; + + appservice = { + port = 8009; + + bot.username = facebookBotUsername; + }; + + bridge.permissions."@${username}:server" = "user"; + }; + }; + + services.mautrix-meta.instances.instagram = { + enable = true; + + settings = { + homeserver = { + address = homeserverUrl; + domain = homeserverDomain; + }; + + appservice = { + port = 8010; + + bot.username = instagramBotUsername; + }; + + bridge.permissions."@${username}:server" = "user"; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8008 ]; + }; + + client = { pkgs, ... }: { + environment.systemPackages = [ + (pkgs.writers.writePython3Bin "register_user" + { + libraries = [ pkgs.python3Packages.matrix-nio ]; + flakeIgnore = [ + # We don't live in the dark ages anymore. + # Languages like Python that are whitespace heavy will overrun + # 79 characters.. + "E501" + ]; + } '' + import sys + import asyncio + + from nio import AsyncClient + + + async def run(username: str, homeserver: str): + matrix = AsyncClient(homeserver) + + response = await matrix.register(username, "foobar") + print("Matrix register response: ", response) + + + if __name__ == "__main__": + asyncio.run(run(sys.argv[1], sys.argv[2])) + '' + ) + (pkgs.writers.writePython3Bin "do_test" + { + libraries = [ pkgs.python3Packages.matrix-nio ]; + flakeIgnore = [ + # We don't live in the dark ages anymore. + # Languages like Python that are whitespace heavy will overrun + # 79 characters.. + "E501" + ]; + } '' + import sys + import functools + import asyncio + + from nio import AsyncClient, RoomMessageNotice, RoomCreateResponse, RoomInviteResponse + + + async def message_callback(matrix: AsyncClient, msg: str, _r, e): + print("Received matrix text message: ", e) + assert msg in e.body + exit(0) # Success! + + + async def run(username: str, bot_username: str, homeserver: str): + matrix = AsyncClient(homeserver, f"@{username}:${homeserverDomain}") + + response = await matrix.login("foobar") + print("Matrix login response: ", response) + + # Open a DM with the bridge bot + response = await matrix.room_create() + print("Matrix create room response:", response) + assert isinstance(response, RoomCreateResponse) + room_id = response.room_id + + response = await matrix.room_invite(room_id, f"@{bot_username}:${homeserverDomain}") + assert isinstance(response, RoomInviteResponse) + + callback = functools.partial( + message_callback, matrix, "Hello, I'm an Instagram bridge bot." + ) + matrix.add_event_callback(callback, RoomMessageNotice) + + print("Waiting for matrix message...") + await matrix.sync_forever(timeout=30000) + + + if __name__ == "__main__": + asyncio.run(run(sys.argv[1], sys.argv[2], sys.argv[3])) + '' + ) + ]; + }; + }; + + testScript = '' + def extract_token(data): + stdout = data[1] + stdout = stdout.strip() + line = stdout.split('\n')[-1] + return line.split(':')[-1].strip("\" '\n") + + def get_token_from(token, file): + data = server.execute(f"cat {file} | grep {token}") + return extract_token(data) + + def get_as_token_from(file): + return get_token_from("as_token", file) + + def get_hs_token_from(file): + return get_token_from("hs_token", file) + + config_yaml = "/var/lib/mautrix-meta-facebook/config.yaml" + registration_yaml = "/var/lib/mautrix-meta-facebook/meta-registration.yaml" + + start_all() + + with subtest("wait for bridges and homeserver"): + # bridge + server.wait_for_unit("mautrix-meta-facebook.service") + server.wait_for_unit("mautrix-meta-instagram.service") + + # homeserver + server.wait_for_unit("matrix-synapse.service") + + server.wait_for_open_port(8008) + # Bridges only open the port after they contact the homeserver + server.wait_for_open_port(8009) + server.wait_for_open_port(8010) + + with subtest("register user"): + client.succeed("register_user ${username} ${homeserverUrl} >&2") + + with subtest("ensure messages can be exchanged"): + client.succeed("do_test ${username} ${facebookBotUsername} ${homeserverUrl} >&2") + client.succeed("do_test ${username} ${instagramBotUsername} ${homeserverUrl} >&2") + + with subtest("ensure as_token and hs_token stays same after restart"): + generated_as_token_facebook = get_as_token_from(config_yaml) + generated_hs_token_facebook = get_hs_token_from(config_yaml) + + generated_as_token_facebook_registration = get_as_token_from(registration_yaml) + generated_hs_token_facebook_registration = get_hs_token_from(registration_yaml) + + # Indirectly checks the as token is not set to something like empty string or "null" + assert len(generated_as_token_facebook) > 20, f"as_token ({generated_as_token_facebook}) is too short, something went wrong" + assert len(generated_hs_token_facebook) > 20, f"hs_token ({generated_hs_token_facebook}) is too short, something went wrong" + + assert generated_as_token_facebook == generated_as_token_facebook_registration, f"as_token should be the same in registration ({generated_as_token_facebook_registration}) and configuration ({generated_as_token_facebook}) files" + assert generated_hs_token_facebook == generated_hs_token_facebook_registration, f"hs_token should be the same in registration ({generated_hs_token_facebook_registration}) and configuration ({generated_hs_token_facebook}) files" + + server.systemctl("restart mautrix-meta-facebook") + server.systemctl("restart mautrix-meta-instagram") + + server.wait_for_open_port(8009) + server.wait_for_open_port(8010) + + new_as_token_facebook = get_as_token_from(config_yaml) + new_hs_token_facebook = get_hs_token_from(config_yaml) + + assert generated_as_token_facebook == new_as_token_facebook, f"as_token should stay the same after restart inside the configuration file (is: {new_as_token_facebook}, was: {generated_as_token_facebook})" + assert generated_hs_token_facebook == new_hs_token_facebook, f"hs_token should stay the same after restart inside the configuration file (is: {new_hs_token_facebook}, was: {generated_hs_token_facebook})" + + new_as_token_facebook = get_as_token_from(registration_yaml) + new_hs_token_facebook = get_hs_token_from(registration_yaml) + + assert generated_as_token_facebook == new_as_token_facebook, f"as_token should stay the same after restart inside the registration file (is: {new_as_token_facebook}, was: {generated_as_token_facebook})" + assert generated_hs_token_facebook == new_hs_token_facebook, f"hs_token should stay the same after restart inside the registration file (is: {new_hs_token_facebook}, was: {generated_hs_token_facebook})" + + with subtest("ensure messages can be exchanged after restart"): + client.succeed("do_test ${username} ${instagramBotUsername} ${homeserverUrl} >&2") + client.succeed("do_test ${username} ${facebookBotUsername} ${homeserverUrl} >&2") + ''; + })