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