diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index d800d0619079b..7c2f3c36a2f6a 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -16480,6 +16480,13 @@ githubId = 521306; name = "Rob Glossop"; }; + robin = { + name = "Robin"; + email = "robin@robin.town"; + matrix = "@robin:robin.town"; + github = "robintown"; + githubId = 48614497; + }; roblabla = { email = "robinlambertz+dev@gmail.com"; github = "roblabla"; diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 84314f4becc0a..cd72631a7b41c 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -66,6 +66,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable). +- [mautrix-discord](https://go.mau.fi/mautrix-discord/), a Matrix to Discord hybrid puppeting/relaybot bridge. + - systemd's gateway, upload, and remote services, which provides ways of sending journals across the network. Enable using [services.journald.gateway](#opt-services.journald.gateway.enable), [services.journald.upload](#opt-services.journald.upload.enable), and [services.journald.remote](#opt-services.journald.remote.enable). - [GNS3](https://www.gns3.com/), a network software emulator. Available as [services.gns3-server](#opt-services.gns3-server.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 378921c996949..d3ca0b0597169 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -644,6 +644,7 @@ ./services/matrix/dendrite.nix ./services/matrix/hebbot.nix ./services/matrix/maubot.nix + ./services/matrix/mautrix-discord.nix ./services/matrix/mautrix-facebook.nix ./services/matrix/mautrix-telegram.nix ./services/matrix/mautrix-whatsapp.nix diff --git a/nixos/modules/services/matrix/mautrix-discord.nix b/nixos/modules/services/matrix/mautrix-discord.nix new file mode 100644 index 0000000000000..b173579cc29c3 --- /dev/null +++ b/nixos/modules/services/matrix/mautrix-discord.nix @@ -0,0 +1,151 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.services.mautrix-discord; + dataDir = "/var/lib/mautrix-discord"; + registrationFile = "${dataDir}/discord-registration.yaml"; + settingsFormat = pkgs.formats.yaml { }; + settingsFile = settingsFormat.generate "mautrix-discord-config.yaml" cfg.settings; + runtimeSettingsFile = "${dataDir}/config.yaml"; +in { + options = { + services.mautrix-discord = { + enable = lib.mkEnableOption "Matrix to Discord hybrid puppeting/relaybot bridge"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.mautrix-discord; + defaultText = lib.literalExpression "pkgs.mautrix-discord"; + description = "The mautrix-discord package to use."; + }; + + settings = lib.mkOption rec { + apply = lib.recursiveUpdate default; + inherit (settingsFormat) type; + default = { + homeserver = { + software = "standard"; + }; + + appservice = rec { + database = { + type = "sqlite3"; + uri = "file:${dataDir}/mautrix-discord.db"; + }; + port = 8080; + address = "http://localhost:${toString port}"; + }; + + bridge = { + permissions."*" = "relay"; + double_puppet_server_map = {}; + login_shared_secret_map = {}; + }; + + logging = { + directory = ""; + file_name_format = ""; # Disable file logging + file_date_format = "2006-01-02"; + file_mode = 384; + timestamp_format = "Jan _2, 2006 15:04:05"; + print_level = "warn"; + print_json = false; + file_json = false; + }; + }; + description = '' + Bridge configuration as a Nix attribute set. + + Configuration options should match those described in + [example-config.yaml](https://github.com/mautrix/discord/blob/main/example-config.yaml). + ''; + }; + + serviceDependencies = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = lib.optional config.services.matrix-synapse.enable "matrix-synapse.service"; + defaultText = lib.literalExpression '' + optional config.services.matrix-synapse.enable "matrix-synapse.service" + ''; + description = "List of Systemd services to require and wait for when starting the application service."; + }; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.mautrix-discord = lib.mkIf cfg.enable { + description = "Matrix to Discord hybrid puppeting/relaybot bridge"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ] ++ cfg.serviceDependencies; + after = [ "network-online.target" ] ++ cfg.serviceDependencies; + + preStart = '' + # Generate the appservice's registration file if absent + if [ ! -f '${registrationFile}' ]; then + ${lib.getExe cfg.package} \ + --config '${settingsFile}' \ + --registration '${registrationFile}' \ + --generate-registration + fi + + old_umask=$(umask) + umask 0177 + # Extract the AS and HS tokens from the registration and add them to the settings file + ${lib.getExe pkgs.yq} -y ".appservice.as_token = $(${lib.getExe pkgs.yq} .as_token ${registrationFile}) | .appservice.hs_token = $(${lib.getExe pkgs.yq} .hs_token ${registrationFile})" ${settingsFile} > ${runtimeSettingsFile} + umask $old_umask + ''; + + serviceConfig = + let + needsPrivileges = cfg.settings.appservice.port < 1024; + capabilities = [ (if needsPrivileges then "CAP_NET_BIND_SERVICE" else "") ]; + in { + Type = "simple"; + Restart = "always"; + + DynamicUser = true; + WorkingDirectory = cfg.package; + StateDirectory = baseNameOf dataDir; + UMask = "0007"; + + ExecStart = '' + ${lib.getExe cfg.package} \ + --config ${runtimeSettingsFile} \ + --no-update + ''; + + TemporaryFileSystem = [ "/" ]; + BindPaths = [ dataDir ]; + BindReadOnlyPaths = [ builtins.storeDir ]; + AmbientCapabilities = capabilities; + CapabilityBoundingSet = capabilities; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = !needsPrivileges; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" ]; + }; + }; + }; + + meta.maintainers = with lib.maintainers; [ robin ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e9d3e9935c9a9..cccb1142bc211 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -517,6 +517,7 @@ in { matrix-synapse = handleTest ./matrix/synapse.nix {}; matrix-synapse-workers = handleTest ./matrix/synapse-workers.nix {}; mattermost = handleTest ./mattermost.nix {}; + mautrix-discord = handleTest ./matrix/mautrix-discord.nix {}; mealie = handleTest ./mealie.nix {}; mediamtx = handleTest ./mediamtx.nix {}; mediatomb = handleTest ./mediatomb.nix {}; diff --git a/nixos/tests/matrix/mautrix-discord.nix b/nixos/tests/matrix/mautrix-discord.nix new file mode 100644 index 0000000000000..bd812bdf57ac9 --- /dev/null +++ b/nixos/tests/matrix/mautrix-discord.nix @@ -0,0 +1,154 @@ +import ../make-test-python.nix ({ pkgs, ... }: + let + homeserverUrl = "http://homeserver:8008"; + in + { + name = "mautrix-discord"; + meta.maintainers = pkgs.mautrix-discord.meta.maintainers; + + nodes = { + homeserver = { pkgs, ... }: { + # We'll switch to this once the registration is copied into place + specialisation.running.configuration = { + services.matrix-synapse = { + enable = true; + settings = { + database.name = "sqlite3"; + app_service_config_files = [ "/discord-registration.yaml" ]; + + 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"; + } ]; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8008 ]; + }; + }; + + bridge = { pkgs, ... }: { + services.mautrix-discord = { + enable = true; + + settings = { + homeserver = { + address = homeserverUrl; + domain = "homeserver"; + }; + + appservice = { + address = "http://bridge:8009"; + port = 8009; + }; + + bridge.permissions."@alice:homeserver" = "user"; + }; + }; + + networking.firewall.allowedTCPPorts = [ 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("alice", "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, "@discordbot:homeserver") + assert isinstance(response, RoomInviteResponse) + + callback = functools.partial( + message_callback, matrix, "Hello, I'm a Discord 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 = '' + import pathlib + import os + + start_all() + + with subtest("start the bridge"): + bridge.wait_for_unit("mautrix-discord.service") + + with subtest("copy the registration file"): + bridge.copy_from_vm("/var/lib/mautrix-discord/discord-registration.yaml") + homeserver.copy_from_host( + str(pathlib.Path(os.environ.get("out", os.getcwd())) / "discord-registration.yaml"), "/" + ) + homeserver.succeed("chmod 444 /discord-registration.yaml") + + with subtest("start the homeserver"): + homeserver.succeed( + "/run/current-system/specialisation/running/bin/switch-to-configuration test >&2" + ) + + homeserver.wait_for_unit("matrix-synapse.service") + homeserver.wait_for_open_port(8008) + # Bridge only opens the port after it contacts the homeserver + bridge.wait_for_open_port(8009) + + with subtest("ensure messages can be exchanged"): + client.succeed("do_test ${homeserverUrl} >&2") + ''; + }) diff --git a/pkgs/servers/mautrix-discord/default.nix b/pkgs/servers/mautrix-discord/default.nix index 6ede276ec2b01..ace89444122ef 100644 --- a/pkgs/servers/mautrix-discord/default.nix +++ b/pkgs/servers/mautrix-discord/default.nix @@ -4,6 +4,7 @@ , olm , nix-update-script , testers +, nixosTests , mautrix-discord }: @@ -29,8 +30,11 @@ buildGoModule rec { passthru = { updateScript = nix-update-script { }; - tests.version = testers.testVersion { - package = mautrix-discord; + tests = { + mautrix-discord = nixosTests.mautrix-discord; + version = testers.testVersion { + package = mautrix-discord; + }; }; }; @@ -39,7 +43,7 @@ buildGoModule rec { homepage = "https://github.com/mautrix/discord"; changelog = "https://github.com/mautrix/discord/blob/${src.rev}/CHANGELOG.md"; license = licenses.agpl3Only; - maintainers = with maintainers; [ MoritzBoehme ]; + maintainers = with maintainers; [ MoritzBoehme robin ]; mainProgram = "mautrix-discord"; }; }