grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / misc / nitter.nix
blob40a4b157aee11035d637fa0c3c50a3cba7d3a4ad
1 { config, lib, pkgs, ... }:
2 let
3   cfg = config.services.nitter;
4   configFile = pkgs.writeText "nitter.conf" ''
5     ${lib.generators.toINI {
6       # String values need to be quoted
7       mkKeyValue = lib.generators.mkKeyValueDefault {
8         mkValueString = v:
9           if lib.isString v then "\"" + (lib.escape ["\""] (toString v)) + "\""
10           else lib.generators.mkValueStringDefault {} v;
11       } " = ";
12     } (lib.recursiveUpdate {
13       Server = cfg.server;
14       Cache = cfg.cache;
15       Config = cfg.config // { hmacKey = "@hmac@"; };
16       Preferences = cfg.preferences;
17     } cfg.settings)}
18   '';
19   # `hmac` is a secret used for cryptographic signing of video URLs.
20   # Generate it on first launch, then copy configuration and replace
21   # `@hmac@` with this value.
22   # We are not using sed as it would leak the value in the command line.
23   preStart = pkgs.writers.writePython3 "nitter-prestart" {} ''
24     import os
25     import secrets
27     state_dir = os.environ.get("STATE_DIRECTORY")
28     if not os.path.isfile(f"{state_dir}/hmac"):
29         # Generate hmac on first launch
30         hmac = secrets.token_hex(32)
31         with open(f"{state_dir}/hmac", "w") as f:
32             f.write(hmac)
33     else:
34         # Load previously generated hmac
35         with open(f"{state_dir}/hmac", "r") as f:
36             hmac = f.read()
38     configFile = "${configFile}"
39     with open(configFile, "r") as f_in:
40         with open(f"{state_dir}/nitter.conf", "w") as f_out:
41             f_out.write(f_in.read().replace("@hmac@", hmac))
42   '';
45   imports = [
46     # https://github.com/zedeus/nitter/pull/772
47     (lib.mkRemovedOptionModule [ "services" "nitter" "replaceInstagram" ] "Nitter no longer supports this option as Bibliogram has been discontinued.")
48   ];
50   options = {
51     services.nitter = {
52       enable = lib.mkEnableOption "Nitter, an alternative Twitter front-end";
54       package = lib.mkPackageOption pkgs "nitter" { };
56       server = {
57         address = lib.mkOption {
58           type =  lib.types.str;
59           default = "0.0.0.0";
60           example = "127.0.0.1";
61           description = "The address to listen on.";
62         };
64         port = lib.mkOption {
65           type = lib.types.port;
66           default = 8080;
67           example = 8000;
68           description = "The port to listen on.";
69         };
71         https = lib.mkOption {
72           type = lib.types.bool;
73           default = false;
74           description = "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS.";
75         };
77         httpMaxConnections = lib.mkOption {
78           type = lib.types.int;
79           default = 100;
80           description = "Maximum number of HTTP connections.";
81         };
83         staticDir = lib.mkOption {
84           type = lib.types.path;
85           default = "${cfg.package}/share/nitter/public";
86           defaultText = lib.literalExpression ''"''${config.services.nitter.package}/share/nitter/public"'';
87           description = "Path to the static files directory.";
88         };
90         title = lib.mkOption {
91           type = lib.types.str;
92           default = "nitter";
93           description = "Title of the instance.";
94         };
96         hostname = lib.mkOption {
97           type = lib.types.str;
98           default = "localhost";
99           example = "nitter.net";
100           description = "Hostname of the instance.";
101         };
102       };
104       cache = {
105         listMinutes = lib.mkOption {
106           type = lib.types.int;
107           default = 240;
108           description = "How long to cache list info (not the tweets, so keep it high).";
109         };
111         rssMinutes = lib.mkOption {
112           type = lib.types.int;
113           default = 10;
114           description = "How long to cache RSS queries.";
115         };
117         redisHost = lib.mkOption {
118           type = lib.types.str;
119           default = "localhost";
120           description = "Redis host.";
121         };
123         redisPort = lib.mkOption {
124           type = lib.types.port;
125           default = 6379;
126           description = "Redis port.";
127         };
129         redisConnections = lib.mkOption {
130           type = lib.types.int;
131           default = 20;
132           description = "Redis connection pool size.";
133         };
135         redisMaxConnections = lib.mkOption {
136           type = lib.types.int;
137           default = 30;
138           description = ''
139             Maximum number of connections to Redis.
141             New connections are opened when none are available, but if the
142             pool size goes above this, they are closed when released, do not
143             worry about this unless you receive tons of requests per second.
144           '';
145         };
146       };
148       config = {
149         base64Media = lib.mkOption {
150           type = lib.types.bool;
151           default = false;
152           description = "Use base64 encoding for proxied media URLs.";
153         };
155         enableRSS = lib.mkEnableOption "RSS feeds" // { default = true; };
157         enableDebug = lib.mkEnableOption "request logs and debug endpoints";
159         proxy = lib.mkOption {
160           type = lib.types.str;
161           default = "";
162           description = "URL to a HTTP/HTTPS proxy.";
163         };
165         proxyAuth = lib.mkOption {
166           type = lib.types.str;
167           default = "";
168           description = "Credentials for proxy.";
169         };
171         tokenCount = lib.mkOption {
172           type = lib.types.int;
173           default = 10;
174           description = ''
175             Minimum amount of usable tokens.
177             Tokens are used to authorize API requests, but they expire after
178             ~1 hour, and have a limit of 187 requests. The limit gets reset
179             every 15 minutes, and the pool is filled up so there is always at
180             least tokenCount usable tokens. Only increase this if you receive
181             major bursts all the time.
182           '';
183         };
184       };
186       preferences = {
187         replaceTwitter = lib.mkOption {
188           type = lib.types.str;
189           default = "";
190           example = "nitter.net";
191           description = "Replace Twitter links with links to this instance (blank to disable).";
192         };
194         replaceYouTube = lib.mkOption {
195           type = lib.types.str;
196           default = "";
197           example = "piped.kavin.rocks";
198           description = "Replace YouTube links with links to this instance (blank to disable).";
199         };
201         replaceReddit = lib.mkOption {
202           type = lib.types.str;
203           default = "";
204           example = "teddit.net";
205           description = "Replace Reddit links with links to this instance (blank to disable).";
206         };
208         mp4Playback = lib.mkOption {
209           type = lib.types.bool;
210           default = true;
211           description = "Enable MP4 video playback.";
212         };
214         hlsPlayback = lib.mkOption {
215           type = lib.types.bool;
216           default = false;
217           description = "Enable HLS video streaming (requires JavaScript).";
218         };
220         proxyVideos = lib.mkOption {
221           type = lib.types.bool;
222           default = true;
223           description = "Proxy video streaming through the server (might be slow).";
224         };
226         muteVideos = lib.mkOption {
227           type = lib.types.bool;
228           default = false;
229           description = "Mute videos by default.";
230         };
232         autoplayGifs = lib.mkOption {
233           type = lib.types.bool;
234           default = true;
235           description = "Autoplay GIFs.";
236         };
238         theme = lib.mkOption {
239           type = lib.types.str;
240           default = "Nitter";
241           description = "Instance theme.";
242         };
244         infiniteScroll = lib.mkOption {
245           type = lib.types.bool;
246           default = false;
247           description = "Infinite scrolling (requires JavaScript, experimental!).";
248         };
250         stickyProfile = lib.mkOption {
251           type = lib.types.bool;
252           default = true;
253           description = "Make profile sidebar stick to top.";
254         };
256         bidiSupport = lib.mkOption {
257           type = lib.types.bool;
258           default = false;
259           description = "Support bidirectional text (makes clicking on tweets harder).";
260         };
262         hideTweetStats = lib.mkOption {
263           type = lib.types.bool;
264           default = false;
265           description = "Hide tweet stats (replies, retweets, likes).";
266         };
268         hideBanner = lib.mkOption {
269           type = lib.types.bool;
270           default = false;
271           description = "Hide profile banner.";
272         };
274         hidePins = lib.mkOption {
275           type = lib.types.bool;
276           default = false;
277           description = "Hide pinned tweets.";
278         };
280         hideReplies = lib.mkOption {
281           type = lib.types.bool;
282           default = false;
283           description = "Hide tweet replies.";
284         };
286         squareAvatars = lib.mkOption {
287           type = lib.types.bool;
288           default = false;
289           description = "Square profile pictures.";
290         };
291       };
293       settings = lib.mkOption {
294         type = lib.types.attrs;
295         default = {};
296         description = ''
297           Add settings here to override NixOS module generated settings.
299           Check the official repository for the available settings:
300           https://github.com/zedeus/nitter/blob/master/nitter.example.conf
301         '';
302       };
304       guestAccounts = lib.mkOption {
305         type = lib.types.path;
306         default = "/var/lib/nitter/guest_accounts.jsonl";
307         description = ''
308           Path to the guest accounts file.
310           This file contains a list of guest accounts that can be used to
311           access the instance without logging in. The file is in JSONL format,
312           where each line is a JSON object with the following fields:
314           {"oauth_token":"some_token","oauth_token_secret":"some_secret_key"}
316           See https://github.com/zedeus/nitter/wiki/Guest-Account-Branch-Deployment
317           for more information on guest accounts and how to generate them.
318         '';
319       };
321       redisCreateLocally = lib.mkOption {
322         type = lib.types.bool;
323         default = true;
324         description = "Configure local Redis server for Nitter.";
325       };
327       openFirewall = lib.mkOption {
328         type = lib.types.bool;
329         default = false;
330         description = "Open ports in the firewall for Nitter web interface.";
331       };
332     };
333   };
335   config = lib.mkIf cfg.enable {
336     assertions = [
337       {
338         assertion = !cfg.redisCreateLocally || (cfg.cache.redisHost == "localhost" && cfg.cache.redisPort == 6379);
339         message = "When services.nitter.redisCreateLocally is enabled, you need to use localhost:6379 as a cache server.";
340       }
341     ];
343     systemd.services.nitter = {
344         description = "Nitter (An alternative Twitter front-end)";
345         wantedBy = [ "multi-user.target" ];
346         wants = [ "network-online.target" ];
347         after = [ "network-online.target" ];
348         serviceConfig = {
349           DynamicUser = true;
350           LoadCredential="guestAccountsFile:${cfg.guestAccounts}";
351           StateDirectory = "nitter";
352           Environment = [
353             "NITTER_CONF_FILE=/var/lib/nitter/nitter.conf"
354             "NITTER_ACCOUNTS_FILE=%d/guestAccountsFile"
355           ];
356           # Some parts of Nitter expect `public` folder in working directory,
357           # see https://github.com/zedeus/nitter/issues/414
358           WorkingDirectory = "${cfg.package}/share/nitter";
359           ExecStart = "${cfg.package}/bin/nitter";
360           ExecStartPre = "${preStart}";
361           AmbientCapabilities = lib.mkIf (cfg.server.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
362           Restart = "on-failure";
363           RestartSec = "5s";
364           # Hardening
365           CapabilityBoundingSet = if (cfg.server.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
366           DeviceAllow = [ "" ];
367           LockPersonality = true;
368           MemoryDenyWriteExecute = true;
369           PrivateDevices = true;
370           # A private user cannot have process capabilities on the host's user
371           # namespace and thus CAP_NET_BIND_SERVICE has no effect.
372           PrivateUsers = (cfg.server.port >= 1024);
373           ProcSubset = "pid";
374           ProtectClock = true;
375           ProtectControlGroups = true;
376           ProtectHome = true;
377           ProtectHostname = true;
378           ProtectKernelLogs = true;
379           ProtectKernelModules = true;
380           ProtectKernelTunables = true;
381           ProtectProc = "invisible";
382           RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
383           RestrictNamespaces = true;
384           RestrictRealtime = true;
385           RestrictSUIDSGID = true;
386           SystemCallArchitectures = "native";
387           SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
388           UMask = "0077";
389         };
390     };
392     services.redis.servers.nitter = lib.mkIf (cfg.redisCreateLocally) {
393       enable = true;
394       port = cfg.cache.redisPort;
395     };
397     networking.firewall = lib.mkIf cfg.openFirewall {
398       allowedTCPPorts = [ cfg.server.port ];
399     };
400   };