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