From ce06f43476863da90dc60dcee606d2b6c5a89a8e Mon Sep 17 00:00:00 2001 From: sefidel Date: Wed, 29 Mar 2023 20:54:19 +0900 Subject: project: initial commit --- overlays/README.md | 4 + overlays/default.nix | 4 + overlays/git-daemon-module.nix | 137 ++++++++++++++ overlays/mautrix-signal-module.nix | 196 +++++++++++++++++++ overlays/mautrix-whatsapp-module.nix | 192 +++++++++++++++++++ overlays/mjolnir-module/default.nix | 242 ++++++++++++++++++++++++ overlays/mjolnir-module/mjolnir.md | 110 +++++++++++ overlays/mjolnir-module/pantalaimon-options.nix | 70 +++++++ overlays/mjolnir-package/default.nix | 80 ++++++++ overlays/mjolnir-package/package.json | 69 +++++++ overlays/mjolnir-package/pin.json | 5 + overlays/mjolnir-package/update.sh | 36 ++++ overlays/sliding-sync-module.nix | 87 +++++++++ overlays/sliding-sync.nix | 22 +++ overlays/soju-module.nix | 132 +++++++++++++ 15 files changed, 1386 insertions(+) create mode 100644 overlays/README.md create mode 100644 overlays/default.nix create mode 100644 overlays/git-daemon-module.nix create mode 100644 overlays/mautrix-signal-module.nix create mode 100644 overlays/mautrix-whatsapp-module.nix create mode 100644 overlays/mjolnir-module/default.nix create mode 100644 overlays/mjolnir-module/mjolnir.md create mode 100644 overlays/mjolnir-module/pantalaimon-options.nix create mode 100644 overlays/mjolnir-package/default.nix create mode 100644 overlays/mjolnir-package/package.json create mode 100644 overlays/mjolnir-package/pin.json create mode 100755 overlays/mjolnir-package/update.sh create mode 100644 overlays/sliding-sync-module.nix create mode 100644 overlays/sliding-sync.nix create mode 100644 overlays/soju-module.nix (limited to 'overlays') diff --git a/overlays/README.md b/overlays/README.md new file mode 100644 index 0000000..4d257f8 --- /dev/null +++ b/overlays/README.md @@ -0,0 +1,4 @@ +infra->overlays +=============== + +Nixpkgs overlays or module replacements. diff --git a/overlays/default.nix b/overlays/default.nix new file mode 100644 index 0000000..353d5bd --- /dev/null +++ b/overlays/default.nix @@ -0,0 +1,4 @@ +self: super: { + mjolnir = super.callPackage ./mjolnir-package { }; + sliding-sync = super.callPackage ./sliding-sync.nix { }; +} diff --git a/overlays/git-daemon-module.nix b/overlays/git-daemon-module.nix new file mode 100644 index 0000000..76b395e --- /dev/null +++ b/overlays/git-daemon-module.nix @@ -0,0 +1,137 @@ +{ config, lib, pkgs, ... }: +with lib; +let + + cfg = config.services.gitDaemon; + +in +{ + + ###### interface + + options = { + services.gitDaemon = { + + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Enable Git daemon, which allows public hosting of git repositories + without any access controls. This is mostly intended for read-only access. + + You can allow write access by setting daemon.receivepack configuration + item of the repository to true. This is solely meant for a closed LAN setting + where everybody is friendly. + + If you need any access controls, use something else. + ''; + }; + + basePath = mkOption { + type = types.str; + default = ""; + example = "/srv/git/"; + description = lib.mdDoc '' + Remap all the path requests as relative to the given path. For example, + if you set base-path to /srv/git, then if you later try to pull + git://example.com/hello.git, Git daemon will interpret the path as /srv/git/hello.git. + ''; + }; + + exportAll = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Publish all directories that look like Git repositories (have the objects + and refs subdirectories), even if they do not have the git-daemon-export-ok file. + + If disabled, you need to touch .git/git-daemon-export-ok in each repository + you want the daemon to publish. + + Warning: enabling this without a repository whitelist or basePath + publishes every git repository you have. + ''; + }; + + repositories = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "/srv/git" "/home/user/git/repo2" ]; + description = lib.mdDoc '' + A whitelist of paths of git repositories, or directories containing repositories + all of which would be published. Paths must not end in "/". + + Warning: leaving this empty and enabling exportAll publishes all + repositories in your filesystem or basePath if specified. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = ""; + example = "example.com"; + description = lib.mdDoc "Listen on a specific IP address or hostname."; + }; + + port = mkOption { + type = types.port; + default = 9418; + description = lib.mdDoc "Port to listen on."; + }; + + options = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc "Extra configuration options to be passed to Git daemon."; + }; + + user = mkOption { + type = types.str; + default = "git"; + description = lib.mdDoc "User under which Git daemon would be running."; + }; + + group = mkOption { + type = types.str; + default = "git"; + description = lib.mdDoc "Group under which Git daemon would be running."; + }; + + createUserAndGroup = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Create the specified group and user. + Disable this option if you want to use the existing user + ''; + }; + }; + }; + + ###### implementation + + config = mkIf cfg.enable { + + users.users.${cfg.user} = optionalAttrs (cfg.createUserAndGroup == true) { + uid = config.ids.uids.git; + group = cfg.group; + description = "Git daemon user"; + }; + + users.groups.${cfg.group} = optionalAttrs (cfg.createUserAndGroup == true) { + gid = config.ids.gids.git; + }; + + systemd.services.git-daemon = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + script = "${pkgs.git}/bin/git daemon --reuseaddr " + + (optionalString (cfg.basePath != "") "--base-path=${cfg.basePath} ") + + (optionalString (cfg.listenAddress != "") "--listen=${cfg.listenAddress} ") + + "--port=${toString cfg.port} --user=${cfg.user} --group=${cfg.group} ${cfg.options} " + + "--verbose " + (optionalString cfg.exportAll "--export-all ") + concatStringsSep " " cfg.repositories; + }; + + }; + +} diff --git a/overlays/mautrix-signal-module.nix b/overlays/mautrix-signal-module.nix new file mode 100644 index 0000000..22abbb5 --- /dev/null +++ b/overlays/mautrix-signal-module.nix @@ -0,0 +1,196 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + dataDir = "/var/lib/mautrix-signal"; + registrationFile = "${dataDir}/signal-registration.yaml"; + cfg = config.services.mautrix-signal; + settingsFormat = pkgs.formats.json { }; + settingsFile = + settingsFormat.generate "mautrix-signal-config.json" cfg.settings; + +in +{ + options = { + services.mautrix-signal = { + enable = mkEnableOption (lib.mdDoc "Mautrix-Signal, a Matrix-Signal puppeting bridge."); + + settings = mkOption rec { + apply = recursiveUpdate default; + inherit (settingsFormat) type; + default = { + homeserver = { + software = "standard"; + }; + + appservice = rec { + database = "sqlite:///${dataDir}/mautrix-signal.db"; + database_opts = { }; + hostname = "0.0.0.0"; + port = 8080; + address = "http://localhost:${toString port}"; + }; + + signal.socket_path = config.services.signald.socketPath; + + bridge = { + permissions."*" = "relay"; + relay.whitelist = [ ]; + double_puppet_server_map = { }; + login_shared_secret_map = { }; + }; + + logging = { + version = 1; + + formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s"; + + handlers.console = { + class = "logging.StreamHandler"; + formatter = "precise"; + }; + + loggers = { + mau.level = "INFO"; + + # prevent tokens from leaking in the logs: + # https://github.com/tulir/mautrix-telegram/issues/351 + aiohttp.level = "WARNING"; + }; + + # log to console/systemd instead of file + root = { + level = "INFO"; + handlers = [ "console" ]; + }; + }; + }; + example = literalExpression '' + { + homeserver = { + address = "http://localhost:8008"; + domain = "public-domain.tld"; + }; + + appservice.public = { + prefix = "/public"; + external = "https://public-appservice-address/public"; + }; + + bridge.permissions = { + "example.com" = "full"; + "@admin:example.com" = "admin"; + }; + } + ''; + 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/signal/blob/master/signal/example-config.yaml). + + Secret tokens should be specified using {option}`environmentFile` + instead of this world-readable attribute set. + ''; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc '' + File containing environment variables to be passed to the mautrix-signal service, + in which secret tokens can be specified securely by defining values for e.g. + `MAUTRIX_SIGNAL_APPSERVICE_AS_TOKEN`, + `MAUTRIX_SIGNAL_APPSERVICE_HS_TOKEN`, + + These environment variables can also be used to set other options by + replacing hierarchy levels by `.`, converting the name to uppercase + and prepending `MAUTRIX_SIGNAL_`. + For example, the first value above maps to + {option}`settings.appservice.as_token`. + + The environment variable values can be prefixed with `json::` to have + them be parsed as JSON. For example, `login_shared_secret_map` can be + set as follows: + `MAUTRIX_SIGNAL_BRIDGE_LOGIN_SHARED_SECRET_MAP=json::{"example.com":"secret"}`. + ''; + }; + + serviceDependencies = mkOption { + type = with types; listOf str; + default = optional config.services.matrix-synapse.enable "matrix-synapse.service"; + defaultText = literalExpression '' + optional config.services.matrix-synapse.enable "matrix-synapse.service" + ''; + description = lib.mdDoc '' + List of Systemd services to require and wait for when starting the application service. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + services.signald.enable = true; + + systemd.services.mautrix-signal = { + description = "Mautrix-Signal, a Matrix-Signal puppeting bridge."; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ] ++ cfg.serviceDependencies; + after = [ "network-online.target" ] ++ cfg.serviceDependencies; + path = [ pkgs.lottieconverter pkgs.ffmpeg-full ]; + + # mautrix-signal tries to generate a dotfile in the home directory of + # the running user if using a postgresql database: + # + # File "python3.10/site-packages/asyncpg/connect_utils.py", line 257, in _dot_postgre> + # return (pathlib.Path.home() / '.postgresql' / filename).resolve() + # File "python3.10/pathlib.py", line 1000, in home + # return cls("~").expanduser() + # File "python3.10/pathlib.py", line 1440, in expanduser + # raise RuntimeError("Could not determine home directory.") + # RuntimeError: Could not determine home directory. + environment.HOME = dataDir; + + preStart = '' + # generate the appservice's registration file if absent + if [ ! -f '${registrationFile}' ]; then + ${pkgs.mautrix-signal}/bin/mautrix-signal \ + --generate-registration \ + --base-config='${pkgs.mautrix-signal}/${pkgs.mautrix-signal.pythonModule.sitePackages}/mautrix_signal/example-config.yaml' \ + --config='${settingsFile}' \ + --registration='${registrationFile}' + fi + '' + lib.optionalString (pkgs.mautrix-signal ? alembic) '' + # run automatic database init and migration scripts + ${pkgs.mautrix-signal.alembic}/bin/alembic -x config='${settingsFile}' upgrade head + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + + DynamicUser = true; + SupplementaryGroups = [ "signald" ]; + PrivateTmp = true; + WorkingDirectory = pkgs.mautrix-signal; # necessary for the database migration scripts to be found + StateDirectory = baseNameOf dataDir; + UMask = "0027"; + EnvironmentFile = cfg.environmentFile; + + ExecStart = '' + ${pkgs.mautrix-signal}/bin/mautrix-signal \ + --config='${settingsFile}' + ''; + }; + }; + }; + + # meta.maintainers = with maintainers; [ boppyt ]; +} diff --git a/overlays/mautrix-whatsapp-module.nix b/overlays/mautrix-whatsapp-module.nix new file mode 100644 index 0000000..4cebcb6 --- /dev/null +++ b/overlays/mautrix-whatsapp-module.nix @@ -0,0 +1,192 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + dataDir = "/var/lib/mautrix-whatsapp"; + registrationFile = "${dataDir}/whatsapp-registration.yaml"; + cfg = config.services.mautrix-whatsapp; + settingsFormat = pkgs.formats.json { }; + settingsFile = + settingsFormat.generate "mautrix-whatsapp-config.json" cfg.settings; +in +{ + options = { + services.mautrix-whatsapp = { + enable = mkEnableOption (lib.mdDoc "Mautrix-Whatsapp, a Matrix-Whatsapp puppeting bridge."); + + settings = mkOption rec { + apply = recursiveUpdate default; + inherit (settingsFormat) type; + default = { + homeserver = { + software = "standard"; + }; + + appservice = rec { + database = { + type = "sqlite"; + uri = "sqlite:///${dataDir}/mautrix-whatsapp.db"; + }; + hostname = "0.0.0.0"; + port = 8080; + address = "http://localhost:${toString port}"; + as_token = "$MAUTRIX_WHATSAPP_APPSERVICE_AS_TOKEN"; + hs_token = "$MAUTRIX_WHATSAPP_APPSERVICE_HS_TOKEN"; + }; + + bridge = { + permissions."*" = "relay"; + relay.whitelist = [ ]; + double_puppet_server_map = { }; + login_shared_secret_map = { }; + }; + + logging = { + version = 1; + + formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s"; + + handlers.console = { + class = "logging.StreamHandler"; + formatter = "precise"; + }; + + # log to console/systemd instead of file + file_name_format = null; + + loggers = { + mau.level = "INFO"; + telethon.level = "INFO"; + + # prevent tokens from leaking in the logs: + # https://github.com/tulir/mautrix-telegram/issues/351 + aiohttp.level = "WARNING"; + }; + }; + }; + example = literalExpression '' + { + homeserver = { + address = "http://localhost:8008"; + domain = "public-domain.tld"; + }; + + appservice.public = { + prefix = "/public"; + external = "https://public-appservice-address/public"; + }; + + bridge.permissions = { + "example.com" = "full"; + "@admin:example.com" = "admin"; + }; + } + ''; + 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/whatsapp/blob/master/mautrix_whatsapp/example-config.yaml). + + Secret tokens should be specified using {option}`environmentFile` + instead of this world-readable attribute set. + ''; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc '' + File containing environment variables to be passed to the mautrix-whatsapp service, + in which secret tokens can be specified securely by defining values for e.g. + `MAUTRIX_WHATSAPP_APPSERVICE_AS_TOKEN`, + `MAUTRIX_WHATSAPP_APPSERVICE_HS_TOKEN`, + + For Mautrix-Whatsapp, only AS_TOKEN and HS_TOKEN is available. + ''; + }; + + serviceDependencies = mkOption { + type = with types; listOf str; + default = optional config.services.matrix-synapse.enable "matrix-synapse.service"; + defaultText = literalExpression '' + optional config.services.matrix-synapse.enable "matrix-synapse.service" + ''; + description = lib.mdDoc '' + List of Systemd services to require and wait for when starting the application service. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.mautrix-whatsapp = { + description = "Mautrix-Whatsapp, a Matrix-Whatsapp puppeting bridge."; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ] ++ cfg.serviceDependencies; + after = [ "network-online.target" ] ++ cfg.serviceDependencies; + path = [ pkgs.lottieconverter pkgs.ffmpeg-full ]; + + # mautrix-whatsapp tries to generate a dotfile in the home directory of + # the running user if using a postgresql database: + # + # File "python3.10/site-packages/asyncpg/connect_utils.py", line 257, in _dot_postgre> + # return (pathlib.Path.home() / '.postgresql' / filename).resolve() + # File "python3.10/pathlib.py", line 1000, in home + # return cls("~").expanduser() + # File "python3.10/pathlib.py", line 1440, in expanduser + # raise RuntimeError("Could not determine home directory.") + # RuntimeError: Could not determine home directory. + environment.HOME = dataDir; + + preStart = '' + # generate the appservice's registration file if absent + if [ ! -f '${registrationFile}' ]; then + ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \ + --generate-registration \ + --config='${settingsFile}' \ + --registration='${registrationFile}' + fi + + ${pkgs.envsubst}/bin/envsubst \ + -i ${settingsFile} \ + -o /run/mautrix-whatsapp/config.json + + # wait until dendrite grabs the config + sleep 5 + '' + lib.optionalString (pkgs.mautrix-whatsapp ? alembic) '' + # run automatic database init and migration scripts + ${pkgs.mautrix-whatsapp.alembic}/bin/alembic -x config='${settingsFile}' upgrade head + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + + DynamicUser = true; + Group = "mautrix-whatsapp"; + PrivateTmp = true; + WorkingDirectory = pkgs.mautrix-whatsapp; # necessary for the database migration scripts to be found + StateDirectory = baseNameOf dataDir; + RuntimeDirectory = "mautrix-whatsapp"; + RuntimeDirectoryMode = "0700"; + UMask = "0027"; + EnvironmentFile = cfg.environmentFile; + + ExecStart = '' + ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \ + --config='/run/mautrix-whatsapp/config.json' + ''; + }; + }; + }; + + # meta.maintainers = with maintainers; [ boppyt ]; +} diff --git a/overlays/mjolnir-module/default.nix b/overlays/mjolnir-module/default.nix new file mode 100644 index 0000000..87ed761 --- /dev/null +++ b/overlays/mjolnir-module/default.nix @@ -0,0 +1,242 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.mjolnir; + + yamlConfig = { + inherit (cfg) dataPath managementRoom protectedRooms; + + accessToken = "@ACCESS_TOKEN@"; # will be replaced in "generateConfig" + homeserverUrl = + if cfg.pantalaimon.enable then + "http://${cfg.pantalaimon.options.listenAddress}:${toString cfg.pantalaimon.options.listenPort}" + else + cfg.homeserverUrl; + + rawHomeserverUrl = cfg.homeserverUrl; + + pantalaimon = { + inherit (cfg.pantalaimon) username; + + use = cfg.pantalaimon.enable; + password = "@PANTALAIMON_PASSWORD@"; # will be replaced in "generateConfig" + }; + }; + + moduleConfigFile = pkgs.writeText "module-config.yaml" ( + generators.toYAML { } (filterAttrs (_: v: v != null) + (fold recursiveUpdate { } [ yamlConfig cfg.settings ]))); + + # these config files will be merged one after the other to build the final config + configFiles = [ + "${pkgs.mjolnir}/libexec/mjolnir/deps/mjolnir/config/default.yaml" + moduleConfigFile + ]; + + # this will generate the default.yaml file with all configFiles as inputs and + # replace all secret strings using replace-secret + generateConfig = pkgs.writeShellScript "mjolnir-generate-config" ( + let + yqEvalStr = concatImapStringsSep " * " (pos: _: "select(fileIndex == ${toString (pos - 1)})") configFiles; + yqEvalArgs = concatStringsSep " " configFiles; + in + '' + set -euo pipefail + + umask 077 + + # mjolnir will try to load a config from "./config/default.yaml" in the working directory + # -> let's place the generated config there + mkdir -p ${cfg.dataPath}/config + + # merge all config files into one, overriding settings of the previous one with the next config + # e.g. "eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' filea.yaml fileb.yaml" will merge filea.yaml with fileb.yaml + ${pkgs.yq-go}/bin/yq eval-all -P '${yqEvalStr}' ${yqEvalArgs} > ${cfg.dataPath}/config/default.yaml + + ${optionalString (cfg.accessTokenFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@ACCESS_TOKEN@' '${cfg.accessTokenFile}' ${cfg.dataPath}/config/default.yaml + ''} + ${optionalString (cfg.pantalaimon.passwordFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@PANTALAIMON_PASSWORD@' '${cfg.pantalaimon.passwordFile}' ${cfg.dataPath}/config/default.yaml + ''} + '' + ); +in +{ + options.services.mjolnir = { + enable = mkEnableOption (lib.mdDoc "Mjolnir, a moderation tool for Matrix"); + + homeserverUrl = mkOption { + type = types.str; + default = "https://matrix.org"; + description = lib.mdDoc '' + Where the homeserver is located (client-server URL). + + If `pantalaimon.enable` is `true`, this option will become the homeserver to which `pantalaimon` connects. + The listen address of `pantalaimon` will then become the `homeserverUrl` of `mjolnir`. + ''; + }; + + accessTokenFile = mkOption { + type = with types; nullOr path; + default = null; + description = lib.mdDoc '' + File containing the matrix access token for the `mjolnir` user. + ''; + }; + + pantalaimon = mkOption { + description = lib.mdDoc '' + `pantalaimon` options (enables E2E Encryption support). + + This will create a `pantalaimon` instance with the name "mjolnir". + ''; + default = { }; + type = types.submodule { + options = { + enable = mkEnableOption (lib.mdDoc '' + If true, accessToken is ignored and the username/password below will be + used instead. The access token of the bot will be stored in the dataPath. + ''); + + username = mkOption { + type = types.str; + description = lib.mdDoc "The username to login with."; + }; + + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + description = lib.mdDoc '' + File containing the matrix password for the `mjolnir` user. + ''; + }; + + options = mkOption { + type = types.submodule (import ./pantalaimon-options.nix); + default = { }; + description = lib.mdDoc '' + passthrough additional options to the `pantalaimon` service. + ''; + }; + }; + }; + }; + + dataPath = mkOption { + type = types.path; + default = "/var/lib/mjolnir"; + description = lib.mdDoc '' + The directory the bot should store various bits of information in. + ''; + }; + + managementRoom = mkOption { + type = types.str; + default = "#moderators:example.org"; + description = lib.mdDoc '' + The room ID where people can use the bot. The bot has no access controls, so + anyone in this room can use the bot - secure your room! + This should be a room alias or room ID - not a matrix.to URL. + Note: `mjolnir` is fairly verbose - expect a lot of messages from it. + ''; + }; + + protectedRooms = mkOption { + type = types.listOf types.str; + default = [ ]; + example = literalExpression '' + [ + "https://matrix.to/#/#yourroom:example.org" + "https://matrix.to/#/#anotherroom:example.org" + ] + ''; + description = lib.mdDoc '' + A list of rooms to protect (matrix.to URLs). + ''; + }; + + settings = mkOption { + default = { }; + type = (pkgs.formats.yaml { }).type; + example = literalExpression '' + { + autojoinOnlyIfManager = true; + automaticallyRedactForReasons = [ "spam" "advertising" ]; + } + ''; + description = lib.mdDoc '' + Additional settings (see [mjolnir default config](https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml) for available settings). These settings will override settings made by the module config. + ''; + }; + }; + + config = mkIf config.services.mjolnir.enable { + assertions = [ + { + assertion = !(cfg.pantalaimon.enable && cfg.pantalaimon.passwordFile == null); + message = "Specify pantalaimon.passwordFile"; + } + { + assertion = !(cfg.pantalaimon.enable && cfg.accessTokenFile != null); + message = "Do not specify accessTokenFile when using pantalaimon"; + } + { + assertion = !(!cfg.pantalaimon.enable && cfg.accessTokenFile == null); + message = "Specify accessTokenFile when not using pantalaimon"; + } + ]; + + services.pantalaimon-headless.instances."mjolnir" = mkIf cfg.pantalaimon.enable + { + homeserver = cfg.homeserverUrl; + } // cfg.pantalaimon.options; + + systemd.services.mjolnir = { + description = "mjolnir - a moderation tool for Matrix"; + wants = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ]; + after = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = ''${pkgs.mjolnir}/bin/mjolnir''; + ExecStartPre = [ generateConfig ]; + WorkingDirectory = cfg.dataPath; + StateDirectory = "mjolnir"; + StateDirectoryMode = "0700"; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + NoNewPrivileges = true; + PrivateDevices = true; + User = "mjolnir"; + Restart = "on-failure"; + + /* TODO: wait for #102397 to be resolved. Then load secrets from $CREDENTIALS_DIRECTORY+"/NAME" + DynamicUser = true; + LoadCredential = [] ++ + optionals (cfg.accessTokenFile != null) [ + "access_token:${cfg.accessTokenFile}" + ] ++ + optionals (cfg.pantalaimon.passwordFile != null) [ + "pantalaimon_password:${cfg.pantalaimon.passwordFile}" + ]; + */ + }; + }; + + users = { + users.mjolnir = { + group = "mjolnir"; + isSystemUser = true; + }; + groups.mjolnir = { }; + }; + }; + + meta = { + doc = ./mjolnir.md; + maintainers = with maintainers; [ jojosch ]; + }; +} diff --git a/overlays/mjolnir-module/mjolnir.md b/overlays/mjolnir-module/mjolnir.md new file mode 100644 index 0000000..f6994ee --- /dev/null +++ b/overlays/mjolnir-module/mjolnir.md @@ -0,0 +1,110 @@ +# Mjolnir (Matrix Moderation Tool) {#module-services-mjolnir} + +This chapter will show you how to set up your own, self-hosted +[Mjolnir](https://github.com/matrix-org/mjolnir) instance. + +As an all-in-one moderation tool, it can protect your server from +malicious invites, spam messages, and whatever else you don't want. +In addition to server-level protection, Mjolnir is great for communities +wanting to protect their rooms without having to use their personal +accounts for moderation. + +The bot by default includes support for bans, redactions, anti-spam, +server ACLs, room directory changes, room alias transfers, account +deactivation, room shutdown, and more. + +See the [README](https://github.com/matrix-org/mjolnir#readme) +page and the [Moderator's guide](https://github.com/matrix-org/mjolnir/blob/main/docs/moderators.md) +for additional instructions on how to setup and use Mjolnir. + +For [additional settings](#opt-services.mjolnir.settings) +see [the default configuration](https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml). + +## Mjolnir Setup {#module-services-mjolnir-setup} + +First create a new Room which will be used as a management room for Mjolnir. In +this room, Mjolnir will log possible errors and debugging information. You'll +need to set this Room-ID in [services.mjolnir.managementRoom](#opt-services.mjolnir.managementRoom). + +Next, create a new user for Mjolnir on your homeserver, if not present already. + +The Mjolnir Matrix user expects to be free of any rate limiting. +See [Synapse #6286](https://github.com/matrix-org/synapse/issues/6286) +for an example on how to achieve this. + +If you want Mjolnir to be able to deactivate users, move room aliases, shutdown rooms, etc. +you'll need to make the Mjolnir user a Matrix server admin. + +Now invite the Mjolnir user to the management room. + +It is recommended to use [Pantalaimon](https://github.com/matrix-org/pantalaimon), +so your management room can be encrypted. This also applies if you are looking to moderate an encrypted room. + +To enable the Pantalaimon E2E Proxy for mjolnir, enable +[services.mjolnir.pantalaimon](#opt-services.mjolnir.pantalaimon.enable). This will +autoconfigure a new Pantalaimon instance, which will connect to the homeserver +set in [services.mjolnir.homeserverUrl](#opt-services.mjolnir.homeserverUrl) and Mjolnir itself +will be configured to connect to the new Pantalaimon instance. + +``` +{ + services.mjolnir = { + enable = true; + homeserverUrl = "https://matrix.domain.tld"; + pantalaimon = { + enable = true; + username = "mjolnir"; + passwordFile = "/run/secrets/mjolnir-password"; + }; + protectedRooms = [ + "https://matrix.to/#/!xxx:domain.tld" + ]; + managementRoom = "!yyy:domain.tld"; + }; +} +``` + +### Element Matrix Services (EMS) {#module-services-mjolnir-setup-ems} + +If you are using a managed ["Element Matrix Services (EMS)"](https://ems.element.io/) +server, you will need to consent to the terms and conditions. Upon startup, an error +log entry with a URL to the consent page will be generated. + +## Synapse Antispam Module {#module-services-mjolnir-matrix-synapse-antispam} + +A Synapse module is also available to apply the same rulesets the bot +uses across an entire homeserver. + +To use the Antispam Module, add `matrix-synapse-plugins.matrix-synapse-mjolnir-antispam` +to the Synapse plugin list and enable the `mjolnir.Module` module. + +``` +{ + services.matrix-synapse = { + plugins = with pkgs; [ + matrix-synapse-plugins.matrix-synapse-mjolnir-antispam + ]; + extraConfig = '' + modules: + - module: mjolnir.Module + config: + # Prevent servers/users in the ban lists from inviting users on this + # server to rooms. Default true. + block_invites: true + # Flag messages sent by servers/users in the ban lists as spam. Currently + # this means that spammy messages will appear as empty to users. Default + # false. + block_messages: false + # Remove users from the user directory search by filtering matrix IDs and + # display names by the entries in the user ban list. Default false. + block_usernames: false + # The room IDs of the ban lists to honour. Unlike other parts of Mjolnir, + # this list cannot be room aliases or permalinks. This server is expected + # to already be joined to the room - Mjolnir will not automatically join + # these rooms. + ban_lists: + - "!roomid:example.org" + ''; + }; +} +``` diff --git a/overlays/mjolnir-module/pantalaimon-options.nix b/overlays/mjolnir-module/pantalaimon-options.nix new file mode 100644 index 0000000..3945a70 --- /dev/null +++ b/overlays/mjolnir-module/pantalaimon-options.nix @@ -0,0 +1,70 @@ +{ config, lib, name, ... }: + +with lib; +{ + options = { + dataPath = mkOption { + type = types.path; + default = "/var/lib/pantalaimon-${name}"; + description = lib.mdDoc '' + The directory where `pantalaimon` should store its state such as the database file. + ''; + }; + + logLevel = mkOption { + type = types.enum [ "info" "warning" "error" "debug" ]; + default = "warning"; + description = lib.mdDoc '' + Set the log level of the daemon. + ''; + }; + + homeserver = mkOption { + type = types.str; + example = "https://matrix.org"; + description = lib.mdDoc '' + The URI of the homeserver that the `pantalaimon` proxy should + forward requests to, without the matrix API path but including + the http(s) schema. + ''; + }; + + ssl = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether or not SSL verification should be enabled for outgoing + connections to the homeserver. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc '' + The address where the daemon will listen to client connections + for this homeserver. + ''; + }; + + listenPort = mkOption { + type = types.port; + default = 8009; + description = lib.mdDoc '' + The port where the daemon will listen to client connections for + this homeserver. Note that the listen address/port combination + needs to be unique between different homeservers. + ''; + }; + + extraSettings = mkOption { + type = types.attrs; + default = { }; + description = lib.mdDoc '' + Extra configuration options. See + [pantalaimon(5)](https://github.com/matrix-org/pantalaimon/blob/master/docs/man/pantalaimon.5.md) + for available options. + ''; + }; + }; +} diff --git a/overlays/mjolnir-package/default.nix b/overlays/mjolnir-package/default.nix new file mode 100644 index 0000000..833124b --- /dev/null +++ b/overlays/mjolnir-package/default.nix @@ -0,0 +1,80 @@ +{ lib +, nixosTests +, mkYarnPackage +, fetchYarnDeps +, fetchFromGitHub +, makeWrapper +, nodejs +, pkgs +, matrix-sdk-crypto-nodejs +}: + +let + pin = lib.importJSON ./pin.json; +in +mkYarnPackage rec { + pname = "mjolnir"; + inherit (pin) version; + + src = fetchFromGitHub { + owner = "matrix-org"; + repo = "mjolnir"; + rev = "v${version}"; + sha256 = pin.srcSha256; + }; + + packageJSON = ./package.json; + offlineCache = fetchYarnDeps { + yarnLock = "${src}/yarn.lock"; + sha256 = pin.yarnSha256; + }; + + packageResolutions = { + "@matrix-org/matrix-sdk-crypto-nodejs" = "${matrix-sdk-crypto-nodejs}/lib/node_modules/@matrix-org/matrix-sdk-crypto-nodejs"; + }; + + nativeBuildInputs = [ + makeWrapper + ]; + + buildPhase = '' + runHook preBuild + yarn --offline build + runHook postBuild + ''; + + postInstall = '' + makeWrapper ${nodejs}/bin/node $out/bin/mjolnir \ + --add-flags $out/libexec/mjolnir/deps/mjolnir/lib/index.js + ''; + + doDist = false; + + passthru = { + tests = { + inherit (nixosTests) mjolnir; + }; + updateScript = ./update.sh; + }; + + meta = with lib; { + description = "A moderation tool for Matrix"; + homepage = "https://github.com/matrix-org/mjolnir"; + longDescription = '' + As an all-in-one moderation tool, it can protect your server from + malicious invites, spam messages, and whatever else you don't want. + In addition to server-level protection, Mjolnir is great for communities + wanting to protect their rooms without having to use their personal + accounts for moderation. + + The bot by default includes support for bans, redactions, anti-spam, + server ACLs, room directory changes, room alias transfers, account + deactivation, room shutdown, and more. + + A Synapse module is also available to apply the same rulesets the bot + uses across an entire homeserver. + ''; + license = licenses.asl20; + maintainers = with maintainers; [ jojosch ]; + }; +} diff --git a/overlays/mjolnir-package/package.json b/overlays/mjolnir-package/package.json new file mode 100644 index 0000000..f7ed5ab --- /dev/null +++ b/overlays/mjolnir-package/package.json @@ -0,0 +1,69 @@ +{ + "name": "mjolnir", + "version": "1.6.3", + "description": "A moderation tool for Matrix", + "main": "lib/index.js", + "repository": "git@github.com:matrix-org/mjolnir.git", + "author": "The Matrix.org Foundation C.I.C.", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "tsc", + "postbuild": "rm -rf lib/test/ && cp -r lib/src/* lib/ && rm -rf lib/src/", + "lint": "tslint --project ./tsconfig.json -t stylish", + "start:dev": "yarn build && node --async-stack-traces lib/index.js", + "test": "ts-mocha --project ./tsconfig.json test/commands/**/*.ts", + "test:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json \"test/integration/**/*Test.ts\"", + "test:integration:single": "NODE_ENV=harness npx ts-mocha --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json", + "test:appservice:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --timeout 300000 --project ./tsconfig.json \"test/appservice/integration/**/*Test.ts\"", + "test:appservice:integration:single": "NODE_ENV=harness npx ts-mocha --timeout 300000 --project ./tsconfig.json", + "test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts", + "version": "sed -i '/# version automated/s/[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*/'$npm_package_version'/' synapse_antispam/setup.py && git add synapse_antispam/setup.py && cat synapse_antispam/setup.py" + }, + "devDependencies": { + "@types/config": "^3.3.0", + "@types/crypto-js": "^4.0.2", + "@types/express": "^4.17.13", + "@types/html-to-text": "^8.0.1", + "@types/humanize-duration": "^3.27.1", + "@types/js-yaml": "^4.0.5", + "@types/jsdom": "^16.2.11", + "@types/mocha": "^9.0.0", + "@types/nedb": "^1.8.12", + "@types/node": "^16.7.10", + "@types/pg": "^8.6.5", + "@types/request": "^2.48.8", + "@types/shell-quote": "1.7.1", + "crypto-js": "^4.1.1", + "eslint": "^7.32", + "expect": "^27.0.6", + "mocha": "^9.0.1", + "ts-mocha": "^9.0.2", + "tslint": "^6.1.3", + "typescript": "^4.8.4", + "typescript-formatter": "^7.2" + }, + "dependencies": { + "@sentry/node": "^7.17.2", + "@sentry/tracing": "^7.17.2", + "await-lock": "^2.2.2", + "body-parser": "^1.20.1", + "config": "^3.3.8", + "express": "^4.17", + "html-to-text": "^8.0.0", + "humanize-duration": "^3.27.1", + "humanize-duration-ts": "^2.1.1", + "js-yaml": "^4.1.0", + "jsdom": "^16.6.0", + "matrix-appservice-bridge": "8.0.0", + "parse-duration": "^1.0.2", + "pg": "^8.8.0", + "prom-client": "^14.1.0", + "shell-quote": "^1.7.3", + "ulidx": "^0.3.0", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">=16.0.0" + } +} diff --git a/overlays/mjolnir-package/pin.json b/overlays/mjolnir-package/pin.json new file mode 100644 index 0000000..73953b8 --- /dev/null +++ b/overlays/mjolnir-package/pin.json @@ -0,0 +1,5 @@ +{ + "version": "1.6.4", + "srcSha256": "sha256-/vnojWLpu/fktqPUhAdL1QTESxDwFrBVYAkyF79Fj9w=", + "yarnSha256": "sha256-B4s0CYr5Ihoh4gkckwZ3z0Nb4LMET48WvRXuhk3fpQM=" +} diff --git a/overlays/mjolnir-package/update.sh b/overlays/mjolnir-package/update.sh new file mode 100755 index 0000000..1ada429 --- /dev/null +++ b/overlays/mjolnir-package/update.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i bash -p nix curl jq prefetch-yarn-deps nix-prefetch-github + +if [ "$#" -gt 1 ] || [[ "$1" == -* ]]; then + echo "Regenerates packaging data for mjolnir." + echo "Usage: $0 [git release tag]" + exit 1 +fi + +version=$1 + +set -euo pipefail + +if [ -z "$version" ]; then + version=$(curl "https://api.github.com/repos/matrix-org/mjolnir/releases/latest" | jq -r '.tag_name') +fi + +src="https://raw.githubusercontent.com/matrix-org/mjolnir/$version" +src_hash=$(nix-prefetch-github matrix-org mjolnir --rev ${version} | jq -r .sha256) + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +pushd $tmpdir +curl -O "$src/yarn.lock" +yarn_hash=$(prefetch-yarn-deps yarn.lock) +popd + +curl -O "$src/package.json" +cat > pin.json << EOF +{ + "version": "$version", + "srcHash": "$src_hash", + "yarnHash": "$yarn_hash" +} +EOF diff --git a/overlays/sliding-sync-module.nix b/overlays/sliding-sync-module.nix new file mode 100644 index 0000000..692818b --- /dev/null +++ b/overlays/sliding-sync-module.nix @@ -0,0 +1,87 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.sliding-sync; +in +{ + # TODO: add default values + options.services.sliding-sync = { + enable = lib.mkEnableOption (lib.mdDoc "matrix.org sliding-sync"); + server = lib.mkOption { + type = lib.types.str; + # default = "https://matrix-client.matrix.org" # TODO: required? + description = lib.mdDoc '' + The destination homeserver to talk to (CS API HTTPS URL) + ''; + }; + db = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + The postgres connection string: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING + ''; + }; + bindAddr = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0:8008"; + description = lib.mdDoc '' + The interface and port to listen on. + ''; + }; + secret = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + A secret to use to encrypt access tokens. + Must remain the same for the lifetime of the database. + ''; + }; + pprof = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The bind addr for pprof debugging e.g ':6060'. + If not set, does not listen. + ''; + }; + prom = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The bind addr for Prometheus metrics, + which will be accessible at /metrics at this address. + ''; + }; + jaegerUrl = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The Jaeger URL to send spans to e.g http://localhost:14268/api/traces + If unset does not send OTLP traces. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.sliding-sync = { + description = "MSC3575 Matrix Sliding Sync Proxy"; + after = [ + "network.target" + ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + DynamicUser = true; + ExecStart = + "${pkgs.sliding-sync}/bin/syncv3"; + Restart = "on-failure"; + }; + environment = { + SYNCV3_SERVER = cfg.server; + SYNCV3_DB = cfg.db; + SYNCV3_SECRET = cfg.secret; + SYNCV3_BINDADDR = cfg.bindAddr; + SYNCV3_PPROF = cfg.pprof; + SYNCV3_PROM = cfg.prom; + SYNCV3_JAEGER_URL = cfg.jaegerUrl; + }; + }; + }; +} diff --git a/overlays/sliding-sync.nix b/overlays/sliding-sync.nix new file mode 100644 index 0000000..50c058d --- /dev/null +++ b/overlays/sliding-sync.nix @@ -0,0 +1,22 @@ +{ lib, buildGoModule, fetchFromGitHub }: + +# TODO: needs: +# - tests +# - `meta` attribute + +buildGoModule rec { + pname = "sliding-sync"; + version = "0.99.1"; + src = fetchFromGitHub { + owner = "matrix-org"; + repo = "sliding-sync"; + rev = "v${version}"; + sha256 = "sha256-g1yMGb8taToEFG6N057yPcdZB855r0f6EwnJ98FIiic="; + }; + + vendorHash = "sha256-FmibAVjKeJUrMSlhoE7onLoa4EVjQvjDI4oU4PB5LBE="; + + subPackages = [ + "cmd/syncv3" + ]; +} diff --git a/overlays/soju-module.nix b/overlays/soju-module.nix new file mode 100644 index 0000000..d14082c --- /dev/null +++ b/overlays/soju-module.nix @@ -0,0 +1,132 @@ +# Not an overlay, module replacement +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.soju; + stateDir = "/var/lib/soju"; + listenCfg = concatMapStringsSep "\n" (l: "listen ${l}") cfg.listen; + tlsCfg = optionalString (cfg.tlsCertificate != null) + "tls ${cfg.tlsCertificate} ${cfg.tlsCertificateKey}"; + logCfg = optionalString cfg.enableMessageLogging + "log fs ${stateDir}/logs"; + + configFile = pkgs.writeText "soju.conf" '' + ${listenCfg} + hostname ${cfg.hostName} + ${tlsCfg} + db sqlite3 ${stateDir}/soju.db + ${logCfg} + http-origin ${concatStringsSep " " cfg.httpOrigins} + accept-proxy-ip ${concatStringsSep " " cfg.acceptProxyIP} + + ${cfg.extraConfig} + ''; +in +{ + ###### interface + + options.services.soju = { + enable = mkEnableOption (lib.mdDoc "soju"); + + listen = mkOption { + type = types.listOf types.str; + default = [ ":6697" ]; + description = lib.mdDoc '' + Where soju should listen for incoming connections. See the + `listen` directive in + {manpage}`soju(1)`. + ''; + }; + + hostName = mkOption { + type = types.str; + default = config.networking.hostName; + defaultText = literalExpression "config.networking.hostName"; + description = lib.mdDoc "Server hostname."; + }; + + tlsCertificate = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/host.cert"; + description = lib.mdDoc "Path to server TLS certificate."; + }; + + tlsCertificateKey = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/host.key"; + description = lib.mdDoc "Path to server TLS certificate key."; + }; + + enableMessageLogging = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Whether to enable message logging."; + }; + + httpOrigins = mkOption { + type = types.listOf types.str; + default = [ ]; + description = lib.mdDoc '' + List of allowed HTTP origins for WebSocket listeners. The parameters are + interpreted as shell patterns, see + {manpage}`glob(7)`. + ''; + }; + + acceptProxyIP = mkOption { + type = types.listOf types.str; + default = [ ]; + description = lib.mdDoc '' + Allow the specified IPs to act as a proxy. Proxys have the ability to + overwrite the remote and local connection addresses (via the X-Forwarded-\* + HTTP header fields). The special name "localhost" accepts the loopback + addresses 127.0.0.0/8 and ::1/128. By default, all IPs are rejected. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = lib.mdDoc "Lines added verbatim to the configuration file."; + }; + + extraGroups = mkOption { + type = types.listOf types.str; + default = [ ]; + description = lib.mdDoc "Extra groups for the dynamic user."; + }; + }; + + ###### implementation + + config = mkIf cfg.enable { + assertions = [ + { + assertion = (cfg.tlsCertificate != null) == (cfg.tlsCertificateKey != null); + message = '' + services.soju.tlsCertificate and services.soju.tlsCertificateKey + must both be specified to enable TLS. + ''; + } + ]; + + systemd.services.soju = { + description = "soju IRC bouncer"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + DynamicUser = true; + SupplementaryGroups = cfg.extraGroups; + Restart = "always"; + ExecStart = "${pkgs.soju}/bin/soju -config ${configFile}"; + StateDirectory = "soju"; + }; + }; + }; + + meta.maintainers = with maintainers; [ malvo ]; +} -- cgit 1.4.1