python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / misc / nitter.nix
blob95394d9d2113e2365087c59f8d14fb0ad05cbc39
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.nitter;
7   configFile = pkgs.writeText "nitter.conf" ''
8     ${generators.toINI {
9       # String values need to be quoted
10       mkKeyValue = generators.mkKeyValueDefault {
11         mkValueString = v:
12           if isString v then "\"" + (strings.escape ["\""] (toString v)) + "\""
13           else generators.mkValueStringDefault {} v;
14       } " = ";
15     } (lib.recursiveUpdate {
16       Server = cfg.server;
17       Cache = cfg.cache;
18       Config = cfg.config // { hmacKey = "@hmac@"; };
19       Preferences = cfg.preferences;
20     } cfg.settings)}
21   '';
22   # `hmac` is a secret used for cryptographic signing of video URLs.
23   # Generate it on first launch, then copy configuration and replace
24   # `@hmac@` with this value.
25   # We are not using sed as it would leak the value in the command line.
26   preStart = pkgs.writers.writePython3 "nitter-prestart" {} ''
27     import os
28     import secrets
30     state_dir = os.environ.get("STATE_DIRECTORY")
31     if not os.path.isfile(f"{state_dir}/hmac"):
32         # Generate hmac on first launch
33         hmac = secrets.token_hex(32)
34         with open(f"{state_dir}/hmac", "w") as f:
35             f.write(hmac)
36     else:
37         # Load previously generated hmac
38         with open(f"{state_dir}/hmac", "r") as f:
39             hmac = f.read()
41     configFile = "${configFile}"
42     with open(configFile, "r") as f_in:
43         with open(f"{state_dir}/nitter.conf", "w") as f_out:
44             f_out.write(f_in.read().replace("@hmac@", hmac))
45   '';
48   options = {
49     services.nitter = {
50       enable = mkEnableOption (lib.mdDoc "If enabled, start Nitter.");
52       package = mkOption {
53         default = pkgs.nitter;
54         type = types.package;
55         defaultText = literalExpression "pkgs.nitter";
56         description = lib.mdDoc "The nitter derivation to use.";
57       };
59       server = {
60         address = mkOption {
61           type =  types.str;
62           default = "0.0.0.0";
63           example = "127.0.0.1";
64           description = lib.mdDoc "The address to listen on.";
65         };
67         port = mkOption {
68           type = types.port;
69           default = 8080;
70           example = 8000;
71           description = lib.mdDoc "The port to listen on.";
72         };
74         https = mkOption {
75           type = types.bool;
76           default = false;
77           description = lib.mdDoc "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS.";
78         };
80         httpMaxConnections = mkOption {
81           type = types.int;
82           default = 100;
83           description = lib.mdDoc "Maximum number of HTTP connections.";
84         };
86         staticDir = mkOption {
87           type = types.path;
88           default = "${cfg.package}/share/nitter/public";
89           defaultText = literalExpression ''"''${config.services.nitter.package}/share/nitter/public"'';
90           description = lib.mdDoc "Path to the static files directory.";
91         };
93         title = mkOption {
94           type = types.str;
95           default = "nitter";
96           description = lib.mdDoc "Title of the instance.";
97         };
99         hostname = mkOption {
100           type = types.str;
101           default = "localhost";
102           example = "nitter.net";
103           description = lib.mdDoc "Hostname of the instance.";
104         };
105       };
107       cache = {
108         listMinutes = mkOption {
109           type = types.int;
110           default = 240;
111           description = lib.mdDoc "How long to cache list info (not the tweets, so keep it high).";
112         };
114         rssMinutes = mkOption {
115           type = types.int;
116           default = 10;
117           description = lib.mdDoc "How long to cache RSS queries.";
118         };
120         redisHost = mkOption {
121           type = types.str;
122           default = "localhost";
123           description = lib.mdDoc "Redis host.";
124         };
126         redisPort = mkOption {
127           type = types.port;
128           default = 6379;
129           description = lib.mdDoc "Redis port.";
130         };
132         redisConnections = mkOption {
133           type = types.int;
134           default = 20;
135           description = lib.mdDoc "Redis connection pool size.";
136         };
138         redisMaxConnections = mkOption {
139           type = types.int;
140           default = 30;
141           description = lib.mdDoc ''
142             Maximum number of connections to Redis.
144             New connections are opened when none are available, but if the
145             pool size goes above this, they are closed when released, do not
146             worry about this unless you receive tons of requests per second.
147           '';
148         };
149       };
151       config = {
152         base64Media = mkOption {
153           type = types.bool;
154           default = false;
155           description = lib.mdDoc "Use base64 encoding for proxied media URLs.";
156         };
158         tokenCount = mkOption {
159           type = types.int;
160           default = 10;
161           description = lib.mdDoc ''
162             Minimum amount of usable tokens.
164             Tokens are used to authorize API requests, but they expire after
165             ~1 hour, and have a limit of 187 requests. The limit gets reset
166             every 15 minutes, and the pool is filled up so there is always at
167             least tokenCount usable tokens. Only increase this if you receive
168             major bursts all the time.
169           '';
170         };
171       };
173       preferences = {
174         replaceTwitter = mkOption {
175           type = types.str;
176           default = "";
177           example = "nitter.net";
178           description = lib.mdDoc "Replace Twitter links with links to this instance (blank to disable).";
179         };
181         replaceYouTube = mkOption {
182           type = types.str;
183           default = "";
184           example = "piped.kavin.rocks";
185           description = lib.mdDoc "Replace YouTube links with links to this instance (blank to disable).";
186         };
188         replaceInstagram = mkOption {
189           type = types.str;
190           default = "";
191           description = lib.mdDoc "Replace Instagram links with links to this instance (blank to disable).";
192         };
194         mp4Playback = mkOption {
195           type = types.bool;
196           default = true;
197           description = lib.mdDoc "Enable MP4 video playback.";
198         };
200         hlsPlayback = mkOption {
201           type = types.bool;
202           default = false;
203           description = lib.mdDoc "Enable HLS video streaming (requires JavaScript).";
204         };
206         proxyVideos = mkOption {
207           type = types.bool;
208           default = true;
209           description = lib.mdDoc "Proxy video streaming through the server (might be slow).";
210         };
212         muteVideos = mkOption {
213           type = types.bool;
214           default = false;
215           description = lib.mdDoc "Mute videos by default.";
216         };
218         autoplayGifs = mkOption {
219           type = types.bool;
220           default = true;
221           description = lib.mdDoc "Autoplay GIFs.";
222         };
224         theme = mkOption {
225           type = types.str;
226           default = "Nitter";
227           description = lib.mdDoc "Instance theme.";
228         };
230         infiniteScroll = mkOption {
231           type = types.bool;
232           default = false;
233           description = lib.mdDoc "Infinite scrolling (requires JavaScript, experimental!).";
234         };
236         stickyProfile = mkOption {
237           type = types.bool;
238           default = true;
239           description = lib.mdDoc "Make profile sidebar stick to top.";
240         };
242         bidiSupport = mkOption {
243           type = types.bool;
244           default = false;
245           description = lib.mdDoc "Support bidirectional text (makes clicking on tweets harder).";
246         };
248         hideTweetStats = mkOption {
249           type = types.bool;
250           default = false;
251           description = lib.mdDoc "Hide tweet stats (replies, retweets, likes).";
252         };
254         hideBanner = mkOption {
255           type = types.bool;
256           default = false;
257           description = lib.mdDoc "Hide profile banner.";
258         };
260         hidePins = mkOption {
261           type = types.bool;
262           default = false;
263           description = lib.mdDoc "Hide pinned tweets.";
264         };
266         hideReplies = mkOption {
267           type = types.bool;
268           default = false;
269           description = lib.mdDoc "Hide tweet replies.";
270         };
271       };
273       settings = mkOption {
274         type = types.attrs;
275         default = {};
276         description = lib.mdDoc ''
277           Add settings here to override NixOS module generated settings.
279           Check the official repository for the available settings:
280           https://github.com/zedeus/nitter/blob/master/nitter.example.conf
281         '';
282       };
284       redisCreateLocally = mkOption {
285         type = types.bool;
286         default = true;
287         description = lib.mdDoc "Configure local Redis server for Nitter.";
288       };
290       openFirewall = mkOption {
291         type = types.bool;
292         default = false;
293         description = lib.mdDoc "Open ports in the firewall for Nitter web interface.";
294       };
295     };
296   };
298   config = mkIf cfg.enable {
299     assertions = [
300       {
301         assertion = !cfg.redisCreateLocally || (cfg.cache.redisHost == "localhost" && cfg.cache.redisPort == 6379);
302         message = "When services.nitter.redisCreateLocally is enabled, you need to use localhost:6379 as a cache server.";
303       }
304     ];
306     systemd.services.nitter = {
307         description = "Nitter (An alternative Twitter front-end)";
308         wantedBy = [ "multi-user.target" ];
309         after = [ "network.target" ];
310         serviceConfig = {
311           DynamicUser = true;
312           StateDirectory = "nitter";
313           Environment = [ "NITTER_CONF_FILE=/var/lib/nitter/nitter.conf" ];
314           # Some parts of Nitter expect `public` folder in working directory,
315           # see https://github.com/zedeus/nitter/issues/414
316           WorkingDirectory = "${cfg.package}/share/nitter";
317           ExecStart = "${cfg.package}/bin/nitter";
318           ExecStartPre = "${preStart}";
319           AmbientCapabilities = lib.mkIf (cfg.server.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
320           Restart = "on-failure";
321           RestartSec = "5s";
322           # Hardening
323           CapabilityBoundingSet = if (cfg.server.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
324           DeviceAllow = [ "" ];
325           LockPersonality = true;
326           MemoryDenyWriteExecute = true;
327           PrivateDevices = true;
328           # A private user cannot have process capabilities on the host's user
329           # namespace and thus CAP_NET_BIND_SERVICE has no effect.
330           PrivateUsers = (cfg.server.port >= 1024);
331           ProcSubset = "pid";
332           ProtectClock = true;
333           ProtectControlGroups = true;
334           ProtectHome = true;
335           ProtectHostname = true;
336           ProtectKernelLogs = true;
337           ProtectKernelModules = true;
338           ProtectKernelTunables = true;
339           ProtectProc = "invisible";
340           RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
341           RestrictNamespaces = true;
342           RestrictRealtime = true;
343           RestrictSUIDSGID = true;
344           SystemCallArchitectures = "native";
345           SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
346           UMask = "0077";
347         };
348     };
350     services.redis.servers.nitter = lib.mkIf (cfg.redisCreateLocally) {
351       enable = true;
352       port = cfg.cache.redisPort;
353     };
355     networking.firewall = mkIf cfg.openFirewall {
356       allowedTCPPorts = [ cfg.server.port ];
357     };
358   };