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}