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