{ config, lib, pkgs, ... }: let inherit (lib.modules) mkIf mkDefault; inherit (lib.trivial) boolToString; inherit (lib.attrsets) mapAttrsToList optionalAttrs; inherit (lib.strings) optionalString concatStringsSep; inherit (pkgs) writeText; cfg = config.services.piped; backendConfig = cfg.backend; nginxConfig = backendConfig.nginx; databaseConfig = backendConfig.database; proxyConfig = cfg.proxy; frontendConfig = cfg.frontend; backendSettings = backendConfig.settings; sedEscapeSlashes = "sed 's#/#\\\/#'"; backendConfigAttrs = { PORT = backendConfig.internalPort; PROXY_PART = "https://${proxyConfig.domain}"; API_URL = "https://${backendConfig.domain}"; FRONTEND_URL = "https://${frontendConfig.domain}"; DISABLE_REGISTRATION = backendSettings.disableRegistrations; FEED_RETENTION = backendSettings.feedRetentionDays; SUBSCRIPTIONS_EXPIRY = backendSettings.subscriptionRetentionDays; HTTP_WORKERS = backendSettings.httpWorkers; SPONSORBLOCK_SERVERS = concatStringsSep "," backendSettings.sponsorblockServers; DISABLE_LBRY = backendSettings.disableLBRYStreams; COMPROMISED_PASSWORD_CHECK = backendSettings.enableCompromisedPasswordCheck; DISABLE_RYD = backendConfig.ryd.disable; RYD_PROXY_URL = backendConfig.ryd.apiURL; SENTRY_DSN = backendSettings.sentryDSN; "hibernate.connection.url" = let inherit (databaseConfig) host port name databaseOptions; in "jdbc:postgresql://${host}:${toString port}/${name}${databaseOptions}"; "hibernate.connection.driver_class" = databaseConfig.driverClass; "hibernate.dialect" = databaseConfig.dialect; "hibernate.connection.username" = "${databaseConfig.username}"; } // (optionalAttrs databaseConfig.usePassword { "hibernate.connection.password" = if databaseConfig.passwordFile == null then databaseConfig.password else "DATABASE_PASSWORD_PLACEHOLDER"; }) // (optionalAttrs backendConfig.captcha.enable { CAPTCHA_API_URL = backendConfig.captcha.apiURL; # This is substituted in the PreStart of piped-backend.service CAPTCHA_API_KEY = if backendConfig.captcha.apiKeyFile != null then "CAPTCHA_API_KEY_FILE_PLACEHOLDER" else backendSettings.apiKey; }) // (optionalAttrs backendConfig.federation.enable { MATRIX_SERVER = backendConfig.federation.matrixServerAddr; # also substituted MATRIX_TOKEN = if backendConfig.federation.matrixTokenFile != "" then "MATRIX_TOKEN_FILE_PLACEHOLDER" else backendConfig.federation.matrixToken; }) // backendSettings.extraSettings; cfgValueToString = value: if builtins.isBool value then boolToString value else toString value; cfgToLine = key: value: "${key}:${cfgValueToString value}"; cfgToString = config: (concatStringsSep "\n" (mapAttrsToList cfgToLine config)); backendConfigFile = writeText "config.properties" (cfgToString backendConfigAttrs); in { config = mkIf (backendConfig.enable) { systemd.tmpfiles.rules = [ "d /run/piped-backend - piped piped" "f /run/piped-backend/config.properties 660 piped piped" ]; # I ran into some weird issues with ExecStartPre in piped-backend for this script # so moved it to its own unit to run before # some sort of syscall issue with systemd.services.piped-backend-config-init = { serviceConfig.User = "piped"; wantedBy = ["piped-backend.service"]; script = let confFile = "/run/piped-backend/config.properties"; in '' cat ${backendConfigFile} > ${confFile} ${optionalString (backendConfig.captcha.enable && backendConfig.captcha.apiKeyFile != null) '' VALUE="$(cat ${backendConfig.captcha.apiKeyFile} | ${sedEscapeSlashes})" sed -i "s/CAPTCHA_API_KEY_FILE_PLACEHOLDER/$VALUE/" ${confFile} ''} ${optionalString (backendConfig.federation.enable && backendConfig.federation.matrixTokenFile != null) '' VALUE="$(cat ${backendConfig.federation.matrixTokenFile} | ${sedEscapeSlashes})" sed -i "s/MATRIX_TOKEN_FILE_PLACEHOLDER/$VALUE/" ${confFile} ''} ${optionalString (databaseConfig.passwordFile != null) '' VALUE=$(cat ${databaseConfig.passwordFile} | ${sedEscapeSlashes}) sed -i "s/DATABASE_PASSWORD_PLACEHOLDER/$VALUE/" ${confFile} ''} chown piped:piped ${confFile} ''; }; systemd.services.piped-backend = { wantedBy = ["multi-user.target"]; wants = [ "piped-backend-config-init.service" ]; after = [ "piped-backend-config-init.service" ]; serviceConfig = { ExecStart = "${backendConfig.package}/bin/piped-backend"; RestartSec = "5s"; User = "piped"; WorkingDirectory = "/run/piped-backend"; CapabilityBoundingSet = ""; PrivateDevices = true; PrivateUsers = true; ProtectHome = true; ProtectKernelLogs = true; ProtectProc = "invisible"; RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"]; RestrictNamespaces = true; SystemCallArchitectures = "native"; SystemCallFilter = ["@system-service" "~@privileged" "~@resources"]; }; }; services.postgresql = mkIf (!databaseConfig.disablePostgresDB) { enable = true; ensureUsers = [ { name = databaseConfig.username; ensurePermissions."DATABASE ${databaseConfig.name}" = "ALL PRIVILEGES"; } ]; ensureDatabases = [ databaseConfig.name ]; authentication = mkIf (databaseConfig.password == "") '' host ${databaseConfig.name} ${databaseConfig.username} samehost peer map=${databaseConfig.name} ''; }; services.nginx = mkIf (!nginxConfig.disableNginx) { enable = true; virtualHosts."${backendConfig.domain}" = { forceSSL = mkDefault nginxConfig.forceSSL; enableACME = mkDefault nginxConfig.enableACME; locations."/" = { proxyPass = "http://127.0.0.1:${toString backendConfig.internalPort}"; }; }; }; }; }