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