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}