at 23.11-pre 23 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4let 5 cfg = config.services.tt-rss; 6 7 configVersion = 26; 8 9 dbPort = if cfg.database.port == null 10 then (if cfg.database.type == "pgsql" then 5432 else 3306) 11 else cfg.database.port; 12 13 poolName = "tt-rss"; 14 15 mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql"; 16 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql"; 17 18 tt-rss-config = let 19 password = 20 if (cfg.database.password != null) then 21 "'${(escape ["'" "\\"] cfg.database.password)}'" 22 else if (cfg.database.passwordFile != null) then 23 "file_get_contents('${cfg.database.passwordFile}')" 24 else 25 null 26 ; 27 in pkgs.writeText "config.php" '' 28 <?php 29 putenv('TTRSS_PHP_EXECUTABLE=${pkgs.php}/bin/php'); 30 31 putenv('TTRSS_LOCK_DIRECTORY=${cfg.root}/lock'); 32 putenv('TTRSS_CACHE_DIR=${cfg.root}/cache'); 33 putenv('TTRSS_ICONS_DIR=${cfg.root}/feed-icons'); 34 putenv('TTRSS_ICONS_URL=feed-icons'); 35 putenv('TTRSS_SELF_URL_PATH=${cfg.selfUrlPath}'); 36 37 putenv('TTRSS_MYSQL_CHARSET=UTF8'); 38 39 putenv('TTRSS_DB_TYPE=${cfg.database.type}'); 40 putenv('TTRSS_DB_HOST=${optionalString (cfg.database.host != null) cfg.database.host}'); 41 putenv('TTRSS_DB_USER=${cfg.database.user}'); 42 putenv('TTRSS_DB_NAME=${cfg.database.name}'); 43 putenv('TTRSS_DB_PASS=' ${optionalString (password != null) ". ${password}"}); 44 putenv('TTRSS_DB_PORT=${toString dbPort}'); 45 46 putenv('TTRSS_AUTH_AUTO_CREATE=${boolToString cfg.auth.autoCreate}'); 47 putenv('TTRSS_AUTH_AUTO_LOGIN=${boolToString cfg.auth.autoLogin}'); 48 49 putenv('TTRSS_FEED_CRYPT_KEY=${escape ["'" "\\"] cfg.feedCryptKey}'); 50 51 52 putenv('TTRSS_SINGLE_USER_MODE=${boolToString cfg.singleUserMode}'); 53 54 putenv('TTRSS_SIMPLE_UPDATE_MODE=${boolToString cfg.simpleUpdateMode}'); 55 56 # Never check for updates - the running version of the code should 57 # be controlled entirely by the version of TT-RSS active in the 58 # current Nix profile. If TT-RSS updates itself to a version 59 # requiring a database schema upgrade, and then the SystemD 60 # tt-rss.service is restarted, the old code copied from the Nix 61 # store will overwrite the updated version, causing the code to 62 # detect the need for a schema "upgrade" (since the schema version 63 # in the database is different than in the code), but the update 64 # schema operation in TT-RSS will do nothing because the schema 65 # version in the database is newer than that in the code. 66 putenv('TTRSS_CHECK_FOR_UPDATES=false'); 67 68 putenv('TTRSS_FORCE_ARTICLE_PURGE=${toString cfg.forceArticlePurge}'); 69 putenv('TTRSS_SESSION_COOKIE_LIFETIME=${toString cfg.sessionCookieLifetime}'); 70 putenv('TTRSS_ENABLE_GZIP_OUTPUT=${boolToString cfg.enableGZipOutput}'); 71 72 putenv('TTRSS_PLUGINS=${builtins.concatStringsSep "," cfg.plugins}'); 73 74 putenv('TTRSS_LOG_DESTINATION=${cfg.logDestination}'); 75 putenv('TTRSS_CONFIG_VERSION=${toString configVersion}'); 76 77 78 putenv('TTRSS_PUBSUBHUBBUB_ENABLED=${boolToString cfg.pubSubHubbub.enable}'); 79 putenv('TTRSS_PUBSUBHUBBUB_HUB=${cfg.pubSubHubbub.hub}'); 80 81 putenv('TTRSS_SPHINX_SERVER=${cfg.sphinx.server}'); 82 putenv('TTRSS_SPHINX_INDEX=${builtins.concatStringsSep "," cfg.sphinx.index}'); 83 84 putenv('TTRSS_ENABLE_REGISTRATION=${boolToString cfg.registration.enable}'); 85 putenv('TTRSS_REG_NOTIFY_ADDRESS=${cfg.registration.notifyAddress}'); 86 putenv('TTRSS_REG_MAX_USERS=${toString cfg.registration.maxUsers}'); 87 88 putenv('TTRSS_SMTP_SERVER=${cfg.email.server}'); 89 putenv('TTRSS_SMTP_LOGIN=${cfg.email.login}'); 90 putenv('TTRSS_SMTP_PASSWORD=${escape ["'" "\\"] cfg.email.password}'); 91 putenv('TTRSS_SMTP_SECURE=${cfg.email.security}'); 92 93 putenv('TTRSS_SMTP_FROM_NAME=${escape ["'" "\\"] cfg.email.fromName}'); 94 putenv('TTRSS_SMTP_FROM_ADDRESS=${escape ["'" "\\"] cfg.email.fromAddress}'); 95 putenv('TTRSS_DIGEST_SUBJECT=${escape ["'" "\\"] cfg.email.digestSubject}'); 96 97 ${cfg.extraConfig} 98 ''; 99 100 # tt-rss and plugins and themes and config.php 101 servedRoot = pkgs.runCommand "tt-rss-served-root" {} '' 102 cp --no-preserve=mode -r ${pkgs.tt-rss} $out 103 cp ${tt-rss-config} $out/config.php 104 ${optionalString (cfg.pluginPackages != []) '' 105 for plugin in ${concatStringsSep " " cfg.pluginPackages}; do 106 cp -r "$plugin"/* "$out/plugins.local/" 107 done 108 ''} 109 ${optionalString (cfg.themePackages != []) '' 110 for theme in ${concatStringsSep " " cfg.themePackages}; do 111 cp -r "$theme"/* "$out/themes.local/" 112 done 113 ''} 114 ''; 115 116 in { 117 118 ###### interface 119 120 options = { 121 122 services.tt-rss = { 123 124 enable = mkEnableOption (lib.mdDoc "tt-rss"); 125 126 root = mkOption { 127 type = types.path; 128 default = "/var/lib/tt-rss"; 129 description = lib.mdDoc '' 130 Root of the application. 131 ''; 132 }; 133 134 user = mkOption { 135 type = types.str; 136 default = "tt_rss"; 137 description = lib.mdDoc '' 138 User account under which both the update daemon and the web-application run. 139 ''; 140 }; 141 142 pool = mkOption { 143 type = types.str; 144 default = "${poolName}"; 145 description = lib.mdDoc '' 146 Name of existing phpfpm pool that is used to run web-application. 147 If not specified a pool will be created automatically with 148 default values. 149 ''; 150 }; 151 152 virtualHost = mkOption { 153 type = types.nullOr types.str; 154 default = "tt-rss"; 155 description = lib.mdDoc '' 156 Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost. 157 ''; 158 }; 159 160 database = { 161 type = mkOption { 162 type = types.enum ["pgsql" "mysql"]; 163 default = "pgsql"; 164 description = lib.mdDoc '' 165 Database to store feeds. Supported are pgsql and mysql. 166 ''; 167 }; 168 169 host = mkOption { 170 type = types.nullOr types.str; 171 default = null; 172 description = lib.mdDoc '' 173 Host of the database. Leave null to use Unix domain socket. 174 ''; 175 }; 176 177 name = mkOption { 178 type = types.str; 179 default = "tt_rss"; 180 description = lib.mdDoc '' 181 Name of the existing database. 182 ''; 183 }; 184 185 user = mkOption { 186 type = types.str; 187 default = "tt_rss"; 188 description = lib.mdDoc '' 189 The database user. The user must exist and has access to 190 the specified database. 191 ''; 192 }; 193 194 password = mkOption { 195 type = types.nullOr types.str; 196 default = null; 197 description = lib.mdDoc '' 198 The database user's password. 199 ''; 200 }; 201 202 passwordFile = mkOption { 203 type = types.nullOr types.str; 204 default = null; 205 description = lib.mdDoc '' 206 The database user's password. 207 ''; 208 }; 209 210 port = mkOption { 211 type = types.nullOr types.port; 212 default = null; 213 description = lib.mdDoc '' 214 The database's port. If not set, the default ports will be provided (5432 215 and 3306 for pgsql and mysql respectively). 216 ''; 217 }; 218 219 createLocally = mkOption { 220 type = types.bool; 221 default = true; 222 description = lib.mdDoc "Create the database and database user locally."; 223 }; 224 }; 225 226 auth = { 227 autoCreate = mkOption { 228 type = types.bool; 229 default = true; 230 description = lib.mdDoc '' 231 Allow authentication modules to auto-create users in tt-rss internal 232 database when authenticated successfully. 233 ''; 234 }; 235 236 autoLogin = mkOption { 237 type = types.bool; 238 default = true; 239 description = lib.mdDoc '' 240 Automatically login user on remote or other kind of externally supplied 241 authentication, otherwise redirect to login form as normal. 242 If set to true, users won't be able to set application language 243 and settings profile. 244 ''; 245 }; 246 }; 247 248 pubSubHubbub = { 249 hub = mkOption { 250 type = types.str; 251 default = ""; 252 description = lib.mdDoc '' 253 URL to a PubSubHubbub-compatible hub server. If defined, "Published 254 articles" generated feed would automatically become PUSH-enabled. 255 ''; 256 }; 257 258 enable = mkOption { 259 type = types.bool; 260 default = false; 261 description = lib.mdDoc '' 262 Enable client PubSubHubbub support in tt-rss. When disabled, tt-rss 263 won't try to subscribe to PUSH feed updates. 264 ''; 265 }; 266 }; 267 268 sphinx = { 269 server = mkOption { 270 type = types.str; 271 default = "localhost:9312"; 272 description = lib.mdDoc '' 273 Hostname:port combination for the Sphinx server. 274 ''; 275 }; 276 277 index = mkOption { 278 type = types.listOf types.str; 279 default = ["ttrss" "delta"]; 280 description = lib.mdDoc '' 281 Index names in Sphinx configuration. Example configuration 282 files are available on tt-rss wiki. 283 ''; 284 }; 285 }; 286 287 registration = { 288 enable = mkOption { 289 type = types.bool; 290 default = false; 291 description = lib.mdDoc '' 292 Allow users to register themselves. Please be aware that allowing 293 random people to access your tt-rss installation is a security risk 294 and potentially might lead to data loss or server exploit. Disabled 295 by default. 296 ''; 297 }; 298 299 notifyAddress = mkOption { 300 type = types.str; 301 default = ""; 302 description = lib.mdDoc '' 303 Email address to send new user notifications to. 304 ''; 305 }; 306 307 maxUsers = mkOption { 308 type = types.int; 309 default = 0; 310 description = lib.mdDoc '' 311 Maximum amount of users which will be allowed to register on this 312 system. 0 - no limit. 313 ''; 314 }; 315 }; 316 317 email = { 318 server = mkOption { 319 type = types.str; 320 default = ""; 321 example = "localhost:25"; 322 description = lib.mdDoc '' 323 Hostname:port combination to send outgoing mail. Blank - use system 324 MTA. 325 ''; 326 }; 327 328 login = mkOption { 329 type = types.str; 330 default = ""; 331 description = lib.mdDoc '' 332 SMTP authentication login used when sending outgoing mail. 333 ''; 334 }; 335 336 password = mkOption { 337 type = types.str; 338 default = ""; 339 description = lib.mdDoc '' 340 SMTP authentication password used when sending outgoing mail. 341 ''; 342 }; 343 344 security = mkOption { 345 type = types.enum ["" "ssl" "tls"]; 346 default = ""; 347 description = lib.mdDoc '' 348 Used to select a secure SMTP connection. Allowed values: ssl, tls, 349 or empty. 350 ''; 351 }; 352 353 fromName = mkOption { 354 type = types.str; 355 default = "Tiny Tiny RSS"; 356 description = lib.mdDoc '' 357 Name for sending outgoing mail. This applies to password reset 358 notifications, digest emails and any other mail. 359 ''; 360 }; 361 362 fromAddress = mkOption { 363 type = types.str; 364 default = ""; 365 description = lib.mdDoc '' 366 Address for sending outgoing mail. This applies to password reset 367 notifications, digest emails and any other mail. 368 ''; 369 }; 370 371 digestSubject = mkOption { 372 type = types.str; 373 default = "[tt-rss] New headlines for last 24 hours"; 374 description = lib.mdDoc '' 375 Subject line for email digests. 376 ''; 377 }; 378 }; 379 380 sessionCookieLifetime = mkOption { 381 type = types.int; 382 default = 86400; 383 description = lib.mdDoc '' 384 Default lifetime of a session (e.g. login) cookie. In seconds, 385 0 means cookie will be deleted when browser closes. 386 ''; 387 }; 388 389 selfUrlPath = mkOption { 390 type = types.str; 391 description = lib.mdDoc '' 392 Full URL of your tt-rss installation. This should be set to the 393 location of tt-rss directory, e.g. http://example.org/tt-rss/ 394 You need to set this option correctly otherwise several features 395 including PUSH, bookmarklets and browser integration will not work properly. 396 ''; 397 example = "http://localhost"; 398 }; 399 400 feedCryptKey = mkOption { 401 type = types.str; 402 default = ""; 403 description = lib.mdDoc '' 404 Key used for encryption of passwords for password-protected feeds 405 in the database. A string of 24 random characters. If left blank, encryption 406 is not used. Requires mcrypt functions. 407 Warning: changing this key will make your stored feed passwords impossible 408 to decrypt. 409 ''; 410 }; 411 412 singleUserMode = mkOption { 413 type = types.bool; 414 default = false; 415 416 description = lib.mdDoc '' 417 Operate in single user mode, disables all functionality related to 418 multiple users and authentication. Enabling this assumes you have 419 your tt-rss directory protected by other means (e.g. http auth). 420 ''; 421 }; 422 423 simpleUpdateMode = mkOption { 424 type = types.bool; 425 default = false; 426 description = lib.mdDoc '' 427 Enables fallback update mode where tt-rss tries to update feeds in 428 background while tt-rss is open in your browser. 429 If you don't have a lot of feeds and don't want to or can't run 430 background processes while not running tt-rss, this method is generally 431 viable to keep your feeds up to date. 432 Still, there are more robust (and recommended) updating methods 433 available, you can read about them here: http://tt-rss.org/wiki/UpdatingFeeds 434 ''; 435 }; 436 437 forceArticlePurge = mkOption { 438 type = types.int; 439 default = 0; 440 description = lib.mdDoc '' 441 When this option is not 0, users ability to control feed purging 442 intervals is disabled and all articles (which are not starred) 443 older than this amount of days are purged. 444 ''; 445 }; 446 447 enableGZipOutput = mkOption { 448 type = types.bool; 449 default = true; 450 description = lib.mdDoc '' 451 Selectively gzip output to improve wire performance. This requires 452 PHP Zlib extension on the server. 453 Enabling this can break tt-rss in several httpd/php configurations, 454 if you experience weird errors and tt-rss failing to start, blank pages 455 after login, or content encoding errors, disable it. 456 ''; 457 }; 458 459 plugins = mkOption { 460 type = types.listOf types.str; 461 default = ["auth_internal" "note"]; 462 description = lib.mdDoc '' 463 List of plugins to load automatically for all users. 464 System plugins have to be specified here. Please enable at least one 465 authentication plugin here (auth_*). 466 Users may enable other user plugins from Preferences/Plugins but may not 467 disable plugins specified in this list. 468 Disabling auth_internal in this list would automatically disable 469 reset password link on the login form. 470 ''; 471 }; 472 473 pluginPackages = mkOption { 474 type = types.listOf types.package; 475 default = []; 476 description = lib.mdDoc '' 477 List of plugins to install. The list elements are expected to 478 be derivations. All elements in this derivation are automatically 479 copied to the `plugins.local` directory. 480 ''; 481 }; 482 483 themePackages = mkOption { 484 type = types.listOf types.package; 485 default = []; 486 description = lib.mdDoc '' 487 List of themes to install. The list elements are expected to 488 be derivations. All elements in this derivation are automatically 489 copied to the `themes.local` directory. 490 ''; 491 }; 492 493 logDestination = mkOption { 494 type = types.enum ["" "sql" "syslog"]; 495 default = "sql"; 496 description = lib.mdDoc '' 497 Log destination to use. Possible values: sql (uses internal logging 498 you can read in Preferences -> System), syslog - logs to system log. 499 Setting this to blank uses PHP logging (usually to http server 500 error.log). 501 ''; 502 }; 503 504 extraConfig = mkOption { 505 type = types.lines; 506 default = ""; 507 description = lib.mdDoc '' 508 Additional lines to append to `config.php`. 509 ''; 510 }; 511 }; 512 }; 513 514 imports = [ 515 (mkRemovedOptionModule ["services" "tt-rss" "checkForUpdates"] '' 516 This option was removed because setting this to true will cause TT-RSS 517 to be unable to start if an automatic update of the code in 518 services.tt-rss.root leads to a database schema upgrade that is not 519 supported by the code active in the Nix store. 520 '') 521 ]; 522 523 ###### implementation 524 525 config = mkIf cfg.enable { 526 527 assertions = [ 528 { 529 assertion = cfg.database.password != null -> cfg.database.passwordFile == null; 530 message = "Cannot set both password and passwordFile"; 531 } 532 ]; 533 534 services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") { 535 ${poolName} = { 536 inherit (cfg) user; 537 phpPackage = pkgs.php81; 538 settings = mapAttrs (name: mkDefault) { 539 "listen.owner" = "nginx"; 540 "listen.group" = "nginx"; 541 "listen.mode" = "0600"; 542 "pm" = "dynamic"; 543 "pm.max_children" = 75; 544 "pm.start_servers" = 10; 545 "pm.min_spare_servers" = 5; 546 "pm.max_spare_servers" = 20; 547 "pm.max_requests" = 500; 548 "catch_workers_output" = 1; 549 }; 550 }; 551 }; 552 553 # NOTE: No configuration is done if not using virtual host 554 services.nginx = mkIf (cfg.virtualHost != null) { 555 enable = true; 556 virtualHosts = { 557 ${cfg.virtualHost} = { 558 root = "${cfg.root}/www"; 559 560 locations."/" = { 561 index = "index.php"; 562 }; 563 564 locations."^~ /feed-icons" = { 565 root = "${cfg.root}"; 566 }; 567 568 locations."~ \\.php$" = { 569 extraConfig = '' 570 fastcgi_split_path_info ^(.+\.php)(/.+)$; 571 fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket}; 572 fastcgi_index index.php; 573 ''; 574 }; 575 }; 576 }; 577 }; 578 579 systemd.tmpfiles.rules = [ 580 "d '${cfg.root}' 0555 ${cfg.user} tt_rss - -" 581 "d '${cfg.root}/lock' 0755 ${cfg.user} tt_rss - -" 582 "d '${cfg.root}/cache' 0755 ${cfg.user} tt_rss - -" 583 "d '${cfg.root}/cache/upload' 0755 ${cfg.user} tt_rss - -" 584 "d '${cfg.root}/cache/images' 0755 ${cfg.user} tt_rss - -" 585 "d '${cfg.root}/cache/export' 0755 ${cfg.user} tt_rss - -" 586 "d '${cfg.root}/feed-icons' 0755 ${cfg.user} tt_rss - -" 587 "L+ '${cfg.root}/www' - - - - ${servedRoot}" 588 ]; 589 590 systemd.services = { 591 phpfpm-tt-rss = mkIf (cfg.pool == "${poolName}") { 592 restartTriggers = [ servedRoot ]; 593 }; 594 595 tt-rss = { 596 description = "Tiny Tiny RSS feeds update daemon"; 597 598 preStart = let 599 callSql = e: 600 if cfg.database.type == "pgsql" then '' 601 ${optionalString (cfg.database.password != null) "PGPASSWORD=${cfg.database.password}"} \ 602 ${optionalString (cfg.database.passwordFile != null) "PGPASSWORD=$(cat ${cfg.database.passwordFile})"} \ 603 ${config.services.postgresql.package}/bin/psql \ 604 -U ${cfg.database.user} \ 605 ${optionalString (cfg.database.host != null) "-h ${cfg.database.host} --port ${toString dbPort}"} \ 606 -c '${e}' \ 607 ${cfg.database.name}'' 608 609 else if cfg.database.type == "mysql" then '' 610 echo '${e}' | ${config.services.mysql.package}/bin/mysql \ 611 -u ${cfg.database.user} \ 612 ${optionalString (cfg.database.password != null) "-p${cfg.database.password}"} \ 613 ${optionalString (cfg.database.host != null) "-h ${cfg.database.host} -P ${toString dbPort}"} \ 614 ${cfg.database.name}'' 615 616 else ""; 617 618 in (optionalString (cfg.database.type == "pgsql") '' 619 exists=$(${callSql "select count(*) > 0 from pg_tables where tableowner = user"} \ 620 | tail -n+3 | head -n-2 | sed -e 's/[ \n\t]*//') 621 622 if [ "$exists" == 'f' ]; then 623 ${callSql "\\i ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"} 624 else 625 echo 'The database contains some data. Leaving it as it is.' 626 fi; 627 '') 628 629 + (optionalString (cfg.database.type == "mysql") '' 630 exists=$(${callSql "select count(*) > 0 from information_schema.tables where table_schema = schema()"} \ 631 | tail -n+2 | sed -e 's/[ \n\t]*//') 632 633 if [ "$exists" == '0' ]; then 634 ${callSql "\\. ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"} 635 else 636 echo 'The database contains some data. Leaving it as it is.' 637 fi; 638 ''); 639 640 serviceConfig = { 641 User = "${cfg.user}"; 642 Group = "tt_rss"; 643 ExecStart = "${pkgs.php}/bin/php ${cfg.root}/www/update.php --daemon --quiet"; 644 Restart = "on-failure"; 645 RestartSec = "60"; 646 SyslogIdentifier = "tt-rss"; 647 }; 648 649 wantedBy = [ "multi-user.target" ]; 650 requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; 651 after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; 652 }; 653 }; 654 655 services.mysql = mkIf mysqlLocal { 656 enable = true; 657 package = mkDefault pkgs.mariadb; 658 ensureDatabases = [ cfg.database.name ]; 659 ensureUsers = [ 660 { 661 name = cfg.user; 662 ensurePermissions = { 663 "${cfg.database.name}.*" = "ALL PRIVILEGES"; 664 }; 665 } 666 ]; 667 }; 668 669 services.postgresql = mkIf pgsqlLocal { 670 enable = mkDefault true; 671 ensureDatabases = [ cfg.database.name ]; 672 ensureUsers = [ 673 { name = cfg.user; 674 ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; }; 675 } 676 ]; 677 }; 678 679 users.users.tt_rss = optionalAttrs (cfg.user == "tt_rss") { 680 description = "tt-rss service user"; 681 isSystemUser = true; 682 group = "tt_rss"; 683 }; 684 685 users.groups.tt_rss = {}; 686 }; 687}