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