From c878ad10fbe97228ecd863856883e3e5f51d409c Mon Sep 17 00:00:00 2001 From: Niko Cantero <97130632+nyabinary@users.noreply.github.com> Date: Mon, 5 May 2025 10:25:57 -0400 Subject: [PATCH] matrix-continuwuity: init at 0.5.0-rc.5; nixos/matrix-continuwuity: init --- .../manual/release-notes/rl-2505.section.md | 2 + nixos/modules/module-list.nix | 1 + .../modules/services/matrix/continuwuity.nix | 268 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/matrix/continuwuity.nix | 104 +++++++ .../ma/matrix-continuwuity/package.nix | 115 ++++++++ 6 files changed, 491 insertions(+) create mode 100644 nixos/modules/services/matrix/continuwuity.nix create mode 100644 nixos/tests/matrix/continuwuity.nix create mode 100644 pkgs/by-name/ma/matrix-continuwuity/package.nix diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index f69de1860a60..35f8f950d2f2 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -78,6 +78,8 @@ - [MaryTTS](https://github.com/marytts/marytts), an open-source, multilingual text-to-speech synthesis system written in pure Java. Available as [services.marytts](options.html#opt-services.marytts). +- [Continuwuity](https://continuwuity.org/), a federated chat server implementing the Matrix protocol, forked from Conduwuit. Available as [services.matrix-continuwuity](#opt-services.matrix-continuwuity.enable). + - [Reposilite](https://reposilite.com), a lightweight and easy-to-use repository manager for Maven-based artifacts in the JVM ecosystem. Available as [services.reposilite](options.html#opt-services.reposilite). - [networking.modemmanager](options.html#opt-networking.modemmanager) has been split out of [networking.networkmanager](options.html#opt-networking.networkmanager). NetworkManager still enables ModemManager by default, but options exist now to run NetworkManager without ModemManager. diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 6e2373694e3c..96d4458dd87f 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -750,6 +750,7 @@ ./services/matrix/appservice-discord.nix ./services/matrix/appservice-irc.nix ./services/matrix/conduit.nix + ./services/matrix/continuwuity.nix ./services/matrix/dendrite.nix ./services/matrix/hebbot.nix ./services/matrix/hookshot.nix diff --git a/nixos/modules/services/matrix/continuwuity.nix b/nixos/modules/services/matrix/continuwuity.nix new file mode 100644 index 000000000000..5170011d3037 --- /dev/null +++ b/nixos/modules/services/matrix/continuwuity.nix @@ -0,0 +1,268 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.matrix-continuwuity; + defaultUser = "continuwuity"; + defaultGroup = "continuwuity"; + + format = pkgs.formats.toml { }; + configFile = format.generate "continuwuity.toml" cfg.settings; +in +{ + meta.maintainers = with lib.maintainers; [ + nyabinary + snaki + ]; + options.services.matrix-continuwuity = { + enable = lib.mkEnableOption "continuwuity"; + + user = lib.mkOption { + type = lib.types.nonEmptyStr; + description = '' + The user {command}`continuwuity` is run as. + ''; + default = defaultUser; + }; + + group = lib.mkOption { + type = lib.types.nonEmptyStr; + description = '' + The group {command}`continuwuity` is run as. + ''; + default = defaultGroup; + }; + + extraEnvironment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + description = "Extra Environment variables to pass to the continuwuity server."; + default = { }; + example = { + RUST_BACKTRACE = "yes"; + }; + }; + + package = lib.mkPackageOption pkgs "matrix-continuwuity" { }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = format.type; + options = { + global.server_name = lib.mkOption { + type = lib.types.nonEmptyStr; + example = "example.com"; + description = "The server_name is the name of this server. It is used as a suffix for user and room ids."; + }; + global.address = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.nonEmptyStr); + default = null; + example = [ + "127.0.0.1" + "::1" + ]; + description = '' + Addresses (IPv4 or IPv6) to listen on for connections by the reverse proxy/tls terminator. + If set to `null`, continuwuity will listen on IPv4 and IPv6 localhost. + Must be `null` if `unix_socket_path` is set. + ''; + }; + global.port = lib.mkOption { + type = lib.types.listOf lib.types.port; + default = [ 6167 ]; + description = '' + The port(s) continuwuity will be running on. + You need to set up a reverse proxy in your web server (e.g. apache or nginx), + so all requests to /_matrix on port 443 and 8448 will be forwarded to the continuwuity + instance running on this port. + ''; + }; + global.unix_socket_path = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Listen on a UNIX socket at the specified path. If listening on a UNIX socket, + listening on an address will be disabled. The `address` option must be set to + `null` (the default value). The option {option}`services.continuwuity.group` must + be set to a group your reverse proxy is part of. + + This will automatically add a system user "continuwuity" to your system if + {option}`services.continuwuity.user` is left at the default, and a "continuwuity" + group if {option}`services.continuwuity.group` is left at the default. + ''; + }; + global.unix_socket_perms = lib.mkOption { + type = lib.types.ints.positive; + default = 660; + description = "The default permissions (in octal) to create the UNIX socket with."; + }; + global.max_request_size = lib.mkOption { + type = lib.types.ints.positive; + default = 20000000; + description = "Max request size in bytes. Don't forget to also change it in the proxy."; + }; + global.allow_registration = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether new users can register on this server. + + Registration with token requires `registration_token` or `registration_token_file` to be set. + + If set to true without a token configured, and + `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse` + is set to true, users can freely register. + ''; + }; + global.allow_encryption = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work."; + }; + global.allow_federation = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether this server federates with other servers. + ''; + }; + global.trusted_servers = lib.mkOption { + type = lib.types.listOf lib.types.nonEmptyStr; + default = [ "matrix.org" ]; + description = '' + Servers listed here will be used to gather public keys of other servers + (notary trusted key servers). + + Currently, continuwuity doesn't support inbound batched key requests, so + this list should only contain other Synapse servers. + + Example: `[ "matrix.org" "constellatory.net" "tchncs.de" ]` + ''; + }; + global.database_path = lib.mkOption { + readOnly = true; + type = lib.types.path; + default = "/var/lib/continuwuity/"; + description = '' + Path to the continuwuity database, the directory where continuwuity will save its data. + Note that database_path cannot be edited because of the service's reliance on systemd StateDir. + ''; + }; + global.allow_announcements_check = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + If enabled, continuwuity will send a simple GET request periodically to + for any new announcements made. + ''; + }; + }; + }; + default = { }; + # TOML does not allow null values, so we use null to omit those fields + apply = lib.filterAttrsRecursive (_: v: v != null); + description = '' + Generates the continuwuity.toml configuration file. Refer to + + for details on supported values. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = !(cfg.settings ? global.unix_socket_path) || !(cfg.settings ? global.address); + message = '' + In `services.continuwuity.settings.global`, `unix_socket_path` and `address` cannot be set at the + same time. + Leave one of the two options unset or explicitly set them to `null`. + ''; + } + { + assertion = cfg.user != defaultUser -> config ? users.users.${cfg.user}; + message = "If `services.continuwuity.user` is changed, the configured user must already exist."; + } + { + assertion = cfg.group != defaultGroup -> config ? users.groups.${cfg.group}; + message = "If `services.continuwuity.group` is changed, the configured group must already exist."; + } + ]; + + users.users = lib.mkIf (cfg.user == defaultUser) { + ${defaultUser} = { + group = cfg.group; + home = cfg.settings.global.database_path; + isSystemUser = true; + }; + }; + + users.groups = lib.mkIf (cfg.group == defaultGroup) { + ${defaultGroup} = { }; + }; + + systemd.services.continuwuity = { + description = "Continuwuity Matrix Server"; + documentation = [ "https://continuwuity.org/" ]; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + environment = lib.mkMerge [ + { CONDUWUIT_CONFIG = configFile; } + cfg.extraEnvironment + ]; + startLimitBurst = 5; + startLimitIntervalSec = 60; + serviceConfig = { + DynamicUser = true; + User = cfg.user; + Group = cfg.group; + + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + PrivateIPC = true; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service @resources" + "~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc" + ]; + SystemCallErrorNumber = "EPERM"; + + StateDirectory = "continuwuity"; + StateDirectoryMode = "0700"; + RuntimeDirectory = "continuwuity"; + RuntimeDirectoryMode = "0750"; + + ExecStart = lib.getExe cfg.package; + Restart = "on-failure"; + RestartSec = 10; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a2924622576b..cf8c67ae2f32 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -783,6 +783,7 @@ in matrix-alertmanager = runTest ./matrix/matrix-alertmanager.nix; matrix-appservice-irc = runTest ./matrix/appservice-irc.nix; matrix-conduit = handleTest ./matrix/conduit.nix { }; + matrix-continuwuity = runTest ./matrix/continuwuity.nix; matrix-synapse = handleTest ./matrix/synapse.nix { }; matrix-synapse-workers = handleTest ./matrix/synapse-workers.nix { }; mautrix-meta-postgres = handleTest ./matrix/mautrix-meta-postgres.nix { }; diff --git a/nixos/tests/matrix/continuwuity.nix b/nixos/tests/matrix/continuwuity.nix new file mode 100644 index 000000000000..308560421c79 --- /dev/null +++ b/nixos/tests/matrix/continuwuity.nix @@ -0,0 +1,104 @@ +{ lib, ... }: +let + name = "continuwuity"; +in +{ + inherit name; + + nodes = { + continuwuity = { + services.matrix-continuwuity = { + enable = true; + settings.global = { + server_name = name; + address = [ "0.0.0.0" ]; + allow_registration = true; + yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true; + }; + extraEnvironment.RUST_BACKTRACE = "yes"; + }; + networking.firewall.allowedTCPPorts = [ 6167 ]; + }; + client = + { pkgs, ... }: + { + environment.systemPackages = [ + (pkgs.writers.writePython3Bin "do_test" { libraries = [ pkgs.python3Packages.matrix-nio ]; } '' + import asyncio + import nio + + + async def main() -> None: + # Connect to continuwuity + client = nio.AsyncClient("http://continuwuity:6167", "alice") + + # Register as user alice + response = await client.register("alice", "my-secret-password") + + # Log in as user alice + response = await client.login("my-secret-password") + + # Create a new room + response = await client.room_create(federate=False) + print("Matrix room create response:", response) + assert isinstance(response, nio.RoomCreateResponse) + room_id = response.room_id + + # Join the room + response = await client.join(room_id) + print("Matrix join response:", response) + assert isinstance(response, nio.JoinResponse) + + # Send a message to the room + response = await client.room_send( + room_id=room_id, + message_type="m.room.message", + content={ + "msgtype": "m.text", + "body": "Hello continuwuity!" + } + ) + print("Matrix room send response:", response) + assert isinstance(response, nio.RoomSendResponse) + + # Sync responses + response = await client.sync(timeout=30000) + print("Matrix sync response:", response) + assert isinstance(response, nio.SyncResponse) + + # Check the message was received by continuwuity + last_message = response.rooms.join[room_id].timeline.events[-1].body + assert last_message == "Hello continuwuity!" + + # Leave the room + response = await client.room_leave(room_id) + print("Matrix room leave response:", response) + assert isinstance(response, nio.RoomLeaveResponse) + + # Close the client + await client.close() + + + if __name__ == "__main__": + asyncio.run(main()) + '') + ]; + }; + }; + + testScript = '' + start_all() + + with subtest("start continuwuity"): + continuwuity.wait_for_unit("continuwuity.service") + continuwuity.wait_for_open_port(6167) + + with subtest("ensure messages can be exchanged"): + client.succeed("do_test >&2") + ''; + + meta.maintainers = with lib.maintainers; [ + nyabinary + snaki + ]; +} diff --git a/pkgs/by-name/ma/matrix-continuwuity/package.nix b/pkgs/by-name/ma/matrix-continuwuity/package.nix new file mode 100644 index 000000000000..fe082b6e8517 --- /dev/null +++ b/pkgs/by-name/ma/matrix-continuwuity/package.nix @@ -0,0 +1,115 @@ +{ + lib, + rustPlatform, + fetchFromGitea, + pkg-config, + bzip2, + zstd, + stdenv, + rocksdb, + nix-update-script, + testers, + matrix-continuwuity, + enableBlurhashing ? true, + # upstream continuwuity enables jemalloc by default, so we follow suit + enableJemalloc ? true, + rust-jemalloc-sys, + enableLiburing ? stdenv.hostPlatform.isLinux, + liburing, + nixosTests, +}: +let + rust-jemalloc-sys' = rust-jemalloc-sys.override { + unprefixed = !stdenv.hostPlatform.isDarwin; + }; + rocksdb' = rocksdb.override { + inherit enableLiburing; + # rocksdb does not support prefixed jemalloc, which is required on darwin + enableJemalloc = enableJemalloc && !stdenv.hostPlatform.isDarwin; + jemalloc = rust-jemalloc-sys'; + }; +in +rustPlatform.buildRustPackage (finalAttrs: { + pname = "matrix-continuwuity"; + version = "0.5.0-rc.5"; + + src = fetchFromGitea { + domain = "forgejo.ellis.link"; + owner = "continuwuation"; + repo = "continuwuity"; + tag = "v${finalAttrs.version}"; + hash = "sha256-Oq2scBu3Ewao828BT1QGffqIqF5WoH9HMXEXKg1YU0o="; + }; + + useFetchCargoVendor = true; + cargoHash = "sha256-bjjGR3++CaDEtlsQj9GgdViCEB5l72sI868uTFBtIwg="; + + nativeBuildInputs = [ + pkg-config + rustPlatform.bindgenHook + ]; + + buildInputs = + [ + bzip2 + zstd + ] + ++ lib.optional enableJemalloc rust-jemalloc-sys' + ++ lib.optional enableLiburing liburing; + + env = { + ZSTD_SYS_USE_PKG_CONFIG = true; + ROCKSDB_INCLUDE_DIR = "${rocksdb'}/include"; + ROCKSDB_LIB_DIR = "${rocksdb'}/lib"; + }; + + buildNoDefaultFeatures = true; + # See https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/Cargo.toml + # for available features. + # We enable all default features except jemalloc, blurhashing, and io_uring, which + # we guard behind our own (default-enabled) flags. + buildFeatures = + [ + "brotli_compression" + "element_hacks" + "gzip_compression" + "media_thumbnail" + "release_max_log_level" + "systemd" + "url_preview" + "zstd_compression" + ] + ++ lib.optional enableBlurhashing "blurhashing" + ++ lib.optional enableJemalloc [ + "jemalloc" + "jemalloc_conf" + ] + ++ lib.optional enableLiburing "io_uring"; + + passthru = { + updateScript = nix-update-script { }; + tests = + { + version = testers.testVersion { + inherit (finalAttrs) version; + package = matrix-continuwuity; + }; + } + // lib.optionalAttrs stdenv.hostPlatform.isLinux { + inherit (nixosTests) matrix-continuwuity; + }; + }; + + meta = { + description = "Matrix homeserver written in Rust, forked from conduwuit"; + homepage = "https://continuwuity.org/"; + changelog = "https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v${finalAttrs.version}"; + license = lib.licenses.asl20; + maintainers = with lib.maintainers; [ + nyabinary + snaki + ]; + # Not a typo, continuwuity is a drop-in replacement for conduwuit. + mainProgram = "conduwuit"; + }; +})