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