1 { config, lib, pkgs, ... }:
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 {
9 if lib.isString v then "\"" + (lib.escape ["\""] (toString v)) + "\""
10 else lib.generators.mkValueStringDefault {} v;
12 } (lib.recursiveUpdate {
15 Config = cfg.config // { hmacKey = "@hmac@"; };
16 Preferences = cfg.preferences;
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" {} ''
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:
34 # Load previously generated hmac
35 with open(f"{state_dir}/hmac", "r") as f:
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))
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.")
52 enable = lib.mkEnableOption "Nitter, an alternative Twitter front-end";
54 package = lib.mkPackageOption pkgs "nitter" { };
57 address = lib.mkOption {
60 example = "127.0.0.1";
61 description = "The address to listen on.";
65 type = lib.types.port;
68 description = "The port to listen on.";
71 https = lib.mkOption {
72 type = lib.types.bool;
74 description = "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS.";
77 httpMaxConnections = lib.mkOption {
80 description = "Maximum number of HTTP connections.";
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.";
90 title = lib.mkOption {
93 description = "Title of the instance.";
96 hostname = lib.mkOption {
98 default = "localhost";
99 example = "nitter.net";
100 description = "Hostname of the instance.";
105 listMinutes = lib.mkOption {
106 type = lib.types.int;
108 description = "How long to cache list info (not the tweets, so keep it high).";
111 rssMinutes = lib.mkOption {
112 type = lib.types.int;
114 description = "How long to cache RSS queries.";
117 redisHost = lib.mkOption {
118 type = lib.types.str;
119 default = "localhost";
120 description = "Redis host.";
123 redisPort = lib.mkOption {
124 type = lib.types.port;
126 description = "Redis port.";
129 redisConnections = lib.mkOption {
130 type = lib.types.int;
132 description = "Redis connection pool size.";
135 redisMaxConnections = lib.mkOption {
136 type = lib.types.int;
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.
149 base64Media = lib.mkOption {
150 type = lib.types.bool;
152 description = "Use base64 encoding for proxied media URLs.";
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;
162 description = "URL to a HTTP/HTTPS proxy.";
165 proxyAuth = lib.mkOption {
166 type = lib.types.str;
168 description = "Credentials for proxy.";
171 tokenCount = lib.mkOption {
172 type = lib.types.int;
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.
187 replaceTwitter = lib.mkOption {
188 type = lib.types.str;
190 example = "nitter.net";
191 description = "Replace Twitter links with links to this instance (blank to disable).";
194 replaceYouTube = lib.mkOption {
195 type = lib.types.str;
197 example = "piped.kavin.rocks";
198 description = "Replace YouTube links with links to this instance (blank to disable).";
201 replaceReddit = lib.mkOption {
202 type = lib.types.str;
204 example = "teddit.net";
205 description = "Replace Reddit links with links to this instance (blank to disable).";
208 mp4Playback = lib.mkOption {
209 type = lib.types.bool;
211 description = "Enable MP4 video playback.";
214 hlsPlayback = lib.mkOption {
215 type = lib.types.bool;
217 description = "Enable HLS video streaming (requires JavaScript).";
220 proxyVideos = lib.mkOption {
221 type = lib.types.bool;
223 description = "Proxy video streaming through the server (might be slow).";
226 muteVideos = lib.mkOption {
227 type = lib.types.bool;
229 description = "Mute videos by default.";
232 autoplayGifs = lib.mkOption {
233 type = lib.types.bool;
235 description = "Autoplay GIFs.";
238 theme = lib.mkOption {
239 type = lib.types.str;
241 description = "Instance theme.";
244 infiniteScroll = lib.mkOption {
245 type = lib.types.bool;
247 description = "Infinite scrolling (requires JavaScript, experimental!).";
250 stickyProfile = lib.mkOption {
251 type = lib.types.bool;
253 description = "Make profile sidebar stick to top.";
256 bidiSupport = lib.mkOption {
257 type = lib.types.bool;
259 description = "Support bidirectional text (makes clicking on tweets harder).";
262 hideTweetStats = lib.mkOption {
263 type = lib.types.bool;
265 description = "Hide tweet stats (replies, retweets, likes).";
268 hideBanner = lib.mkOption {
269 type = lib.types.bool;
271 description = "Hide profile banner.";
274 hidePins = lib.mkOption {
275 type = lib.types.bool;
277 description = "Hide pinned tweets.";
280 hideReplies = lib.mkOption {
281 type = lib.types.bool;
283 description = "Hide tweet replies.";
286 squareAvatars = lib.mkOption {
287 type = lib.types.bool;
289 description = "Square profile pictures.";
293 settings = lib.mkOption {
294 type = lib.types.attrs;
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
304 guestAccounts = lib.mkOption {
305 type = lib.types.path;
306 default = "/var/lib/nitter/guest_accounts.jsonl";
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.
321 redisCreateLocally = lib.mkOption {
322 type = lib.types.bool;
324 description = "Configure local Redis server for Nitter.";
327 openFirewall = lib.mkOption {
328 type = lib.types.bool;
330 description = "Open ports in the firewall for Nitter web interface.";
335 config = lib.mkIf cfg.enable {
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.";
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" ];
350 LoadCredential="guestAccountsFile:${cfg.guestAccounts}";
351 StateDirectory = "nitter";
353 "NITTER_CONF_FILE=/var/lib/nitter/nitter.conf"
354 "NITTER_ACCOUNTS_FILE=%d/guestAccountsFile"
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";
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);
375 ProtectControlGroups = 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" ];
392 services.redis.servers.nitter = lib.mkIf (cfg.redisCreateLocally) {
394 port = cfg.cache.redisPort;
397 networking.firewall = lib.mkIf cfg.openFirewall {
398 allowedTCPPorts = [ cfg.server.port ];