{ config, lib, pkgs, ... }: with lib; let cfg = config.services.tt-rss; configVersion = 26; cacheDir = "cache"; lockDir = "lock"; feedIconsDir = "feed-icons"; dbPort = if cfg.database.port == null then (if cfg.database.type == "pgsql" then 5432 else 3306) else cfg.database.port; poolName = "tt-rss"; mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql"; pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql"; tt-rss-config = pkgs.writeText "config.php" '' plugins.local directory. ''; }; themePackages = mkOption { type = types.listOf types.package; default = []; description = '' List of themes to install. The list elements are expected to be derivations. All elements in this derivation are automatically copied to the themes.local directory. ''; }; logDestination = mkOption { type = types.enum ["" "sql" "syslog"]; default = "sql"; description = '' Log destination to use. Possible values: sql (uses internal logging you can read in Preferences -> System), syslog - logs to system log. Setting this to blank uses PHP logging (usually to http server error.log). ''; }; extraConfig = mkOption { type = types.lines; default = ""; description = '' Additional lines to append to config.php. ''; }; }; }; imports = [ (mkRemovedOptionModule ["services" "tt-rss" "checkForUpdates"] '' This option was removed because setting this to true will cause TT-RSS to be unable to start if an automatic update of the code in services.tt-rss.root leads to a database schema upgrade that is not supported by the code active in the Nix store. '') ]; ###### implementation config = mkIf cfg.enable { assertions = [ { assertion = cfg.database.password != null -> cfg.database.passwordFile == null; message = "Cannot set both password and passwordFile"; } ]; services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") { ${poolName} = { inherit (cfg) user; settings = mapAttrs (name: mkDefault) { "listen.owner" = "nginx"; "listen.group" = "nginx"; "listen.mode" = "0600"; "pm" = "dynamic"; "pm.max_children" = 75; "pm.start_servers" = 10; "pm.min_spare_servers" = 5; "pm.max_spare_servers" = 20; "pm.max_requests" = 500; "catch_workers_output" = 1; }; }; }; # NOTE: No configuration is done if not using virtual host services.nginx = mkIf (cfg.virtualHost != null) { enable = true; virtualHosts = { ${cfg.virtualHost} = { root = "${cfg.root}"; locations."/" = { index = "index.php"; }; locations."~ \\.php$" = { extraConfig = '' fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket}; fastcgi_index index.php; ''; }; }; }; }; systemd.tmpfiles.rules = [ "d '${cfg.root}' 0755 ${cfg.user} tt_rss - -" "Z '${cfg.root}' 0755 ${cfg.user} tt_rss - -" ]; systemd.services.tt-rss = { description = "Tiny Tiny RSS feeds update daemon"; preStart = let callSql = e: if cfg.database.type == "pgsql" then '' ${optionalString (cfg.database.password != null) "PGPASSWORD=${cfg.database.password}"} \ ${optionalString (cfg.database.passwordFile != null) "PGPASSWORD=$(cat ${cfg.database.passwordFile})"} \ ${config.services.postgresql.package}/bin/psql \ -U ${cfg.database.user} \ ${optionalString (cfg.database.host != null) "-h ${cfg.database.host} --port ${toString dbPort}"} \ -c '${e}' \ ${cfg.database.name}'' else if cfg.database.type == "mysql" then '' echo '${e}' | ${config.services.mysql.package}/bin/mysql \ -u ${cfg.database.user} \ ${optionalString (cfg.database.password != null) "-p${cfg.database.password}"} \ ${optionalString (cfg.database.host != null) "-h ${cfg.database.host} -P ${toString dbPort}"} \ ${cfg.database.name}'' else ""; in '' rm -rf "${cfg.root}/*" cp -r "${pkgs.tt-rss}/"* "${cfg.root}" ${optionalString (cfg.pluginPackages != []) '' for plugin in ${concatStringsSep " " cfg.pluginPackages}; do cp -r "$plugin"/* "${cfg.root}/plugins.local/" done ''} ${optionalString (cfg.themePackages != []) '' for theme in ${concatStringsSep " " cfg.themePackages}; do cp -r "$theme"/* "${cfg.root}/themes.local/" done ''} ln -sf "${tt-rss-config}" "${cfg.root}/config.php" chmod -R 755 "${cfg.root}" chmod -R 777 "${cfg.root}/${lockDir}" chmod -R 777 "${cfg.root}/${cacheDir}" chmod -R 777 "${cfg.root}/${feedIconsDir}" '' + (optionalString (cfg.database.type == "pgsql") '' exists=$(${callSql "select count(*) > 0 from pg_tables where tableowner = user"} \ | tail -n+3 | head -n-2 | sed -e 's/[ \n\t]*//') if [ "$exists" == 'f' ]; then ${callSql "\\i ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"} else echo 'The database contains some data. Leaving it as it is.' fi; '') + (optionalString (cfg.database.type == "mysql") '' exists=$(${callSql "select count(*) > 0 from information_schema.tables where table_schema = schema()"} \ | tail -n+2 | sed -e 's/[ \n\t]*//') if [ "$exists" == '0' ]; then ${callSql "\\. ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"} else echo 'The database contains some data. Leaving it as it is.' fi; ''); serviceConfig = { User = "${cfg.user}"; Group = "tt_rss"; ExecStart = "${pkgs.php}/bin/php ${cfg.root}/update.php --daemon --quiet"; Restart = "on-failure"; RestartSec = "60"; SyslogIdentifier = "tt-rss"; }; environment = let password = if (cfg.database.password != null) then "${(escape ["'" "\\"] cfg.database.password)}" else if (cfg.database.passwordFile != null) then "file_get_contents('${cfg.database.passwordFile}'" else "" ; in { TTRSS_PHP_EXECUTABLE = "${pkgs.php}/bin/php"; TTRSS_LOCK_DIRECTORY = "${lockDir}"; TTRSS_CACHE_DIR = "${cacheDir}"; TTRSS_ICONS_DIR = "${feedIconsDir}"; TTRSS_ICONS_URL = "${feedIconsDir}"; TTRSS_SELF_URL_PATH = "${cfg.selfUrlPath}"; TTRSS_MYSQL_CHARSET = "UTF8"; TTRSS_DB_TYPE = "${cfg.database.type}"; TTRSS_DB_HOST = "${optionalString (cfg.database.host != null) cfg.database.host}"; TTRSS_DB_USER = "${cfg.database.user}"; TTRSS_DB_NAME = "${cfg.database.name}"; TTRSS_DB_PASS = "${password}"; TTRSS_DB_PORT = "${toString dbPort}"; TTRSS_AUTH_AUTO_CREATE = "${boolToString cfg.auth.autoCreate}"; TTRSS_AUTH_AUTO_LOGIN = "${boolToString cfg.auth.autoLogin}"; TTRSS_FEED_CRYPT_KEY = "${escape ["'" "\\"] cfg.feedCryptKey}"; TTRSS_SINGLE_USER_MODE = "${boolToString cfg.singleUserMode}"; TTRSS_SIMPLE_UPDATE_MODE = "${boolToString cfg.simpleUpdateMode}"; # Never check for updates - the running version of the code should # be controlled entirely by the version of TT-RSS active in the # current Nix profile. If TT-RSS updates itself to a version # requiring a database schema upgrade, and then the SystemD # tt-rss.service is restarted, the old code copied from the Nix # store will overwrite the updated version, causing the code to # detect the need for a schema "upgrade" (since the schema version # in the database is different than in the code), but the update # schema operation in TT-RSS will do nothing because the schema # version in the database is newer than that in the code. TTRSS_CHECK_FOR_UPDATES = "false"; TTRSS_FORCE_ARTICLE_PURGE = "${toString cfg.forceArticlePurge}"; TTRSS_SESSION_COOKIE_LIFETIME = "${toString cfg.sessionCookieLifetime}"; TTRSS_ENABLE_GZIP_OUTPUT = "${boolToString cfg.enableGZipOutput}"; TTRSS_PLUGINS = "${builtins.concatStringsSep "," cfg.plugins}"; TTRSS_LOG_DESTINATION = "${cfg.logDestination}"; TTRSS_CONFIG_VERSION = "${toString configVersion}"; TTRSS_PUBSUBHUBBUB_ENABLED = "${boolToString cfg.pubSubHubbub.enable}"; TTRSS_PUBSUBHUBBUB_HUB = "${cfg.pubSubHubbub.hub}"; TTRSS_SPHINX_SERVER = "${cfg.sphinx.server}"; TTRSS_SPHINX_INDEX = "${builtins.concatStringsSep "," cfg.sphinx.index}"; TTRSS_ENABLE_REGISTRATION = "${boolToString cfg.registration.enable}"; TTRSS_REG_NOTIFY_ADDRESS = "${cfg.registration.notifyAddress}"; TTRSS_REG_MAX_USERS = "${toString cfg.registration.maxUsers}"; TTRSS_SMTP_SERVER = "${cfg.email.server}"; TTRSS_SMTP_LOGIN = "${cfg.email.login}"; TTRSS_SMTP_PASSWORD = "${escape ["'" "\\"] cfg.email.password}"; TTRSS_SMTP_SECURE = "${cfg.email.security}"; TTRSS_SMTP_FROM_NAME = "${escape ["'" "\\"] cfg.email.fromName}"; TTRSS_SMTP_FROM_ADDRESS = "${escape ["'" "\\"] cfg.email.fromAddress}"; TTRSS_DIGEST_SUBJECT = "${escape ["'" "\\"] cfg.email.digestSubject}"; }; wantedBy = [ "multi-user.target" ]; requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; }; services.mysql = mkIf mysqlLocal { enable = true; package = mkDefault pkgs.mariadb; ensureDatabases = [ cfg.database.name ]; ensureUsers = [ { name = cfg.user; ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; } ]; }; services.postgresql = mkIf pgsqlLocal { enable = mkDefault true; ensureDatabases = [ cfg.database.name ]; ensureUsers = [ { name = cfg.user; ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; }; } ]; }; users.users.tt_rss = optionalAttrs (cfg.user == "tt_rss") { description = "tt-rss service user"; isSystemUser = true; group = "tt_rss"; }; users.groups.tt_rss = {}; }; }