1 { config, lib, pkgs, ... }:
6 cfg = config.services.nitter;
7 configFile = pkgs.writeText "nitter.conf" ''
9 # String values need to be quoted
10 mkKeyValue = generators.mkKeyValueDefault {
12 if isString v then "\"" + (strings.escape ["\""] (toString v)) + "\""
13 else generators.mkValueStringDefault {} v;
15 } (lib.recursiveUpdate {
18 Config = cfg.config // { hmacKey = "@hmac@"; };
19 Preferences = cfg.preferences;
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" {} ''
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:
37 # Load previously generated hmac
38 with open(f"{state_dir}/hmac", "r") as f:
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))
50 enable = mkEnableOption (lib.mdDoc "If enabled, start Nitter.");
53 default = pkgs.nitter;
55 defaultText = literalExpression "pkgs.nitter";
56 description = lib.mdDoc "The nitter derivation to use.";
63 example = "127.0.0.1";
64 description = lib.mdDoc "The address to listen on.";
71 description = lib.mdDoc "The port to listen on.";
77 description = lib.mdDoc "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS.";
80 httpMaxConnections = mkOption {
83 description = lib.mdDoc "Maximum number of HTTP connections.";
86 staticDir = mkOption {
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.";
96 description = lib.mdDoc "Title of the instance.";
101 default = "localhost";
102 example = "nitter.net";
103 description = lib.mdDoc "Hostname of the instance.";
108 listMinutes = mkOption {
111 description = lib.mdDoc "How long to cache list info (not the tweets, so keep it high).";
114 rssMinutes = mkOption {
117 description = lib.mdDoc "How long to cache RSS queries.";
120 redisHost = mkOption {
122 default = "localhost";
123 description = lib.mdDoc "Redis host.";
126 redisPort = mkOption {
129 description = lib.mdDoc "Redis port.";
132 redisConnections = mkOption {
135 description = lib.mdDoc "Redis connection pool size.";
138 redisMaxConnections = mkOption {
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.
152 base64Media = mkOption {
155 description = lib.mdDoc "Use base64 encoding for proxied media URLs.";
158 tokenCount = mkOption {
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.
174 replaceTwitter = mkOption {
177 example = "nitter.net";
178 description = lib.mdDoc "Replace Twitter links with links to this instance (blank to disable).";
181 replaceYouTube = mkOption {
184 example = "piped.kavin.rocks";
185 description = lib.mdDoc "Replace YouTube links with links to this instance (blank to disable).";
188 replaceInstagram = mkOption {
191 description = lib.mdDoc "Replace Instagram links with links to this instance (blank to disable).";
194 mp4Playback = mkOption {
197 description = lib.mdDoc "Enable MP4 video playback.";
200 hlsPlayback = mkOption {
203 description = lib.mdDoc "Enable HLS video streaming (requires JavaScript).";
206 proxyVideos = mkOption {
209 description = lib.mdDoc "Proxy video streaming through the server (might be slow).";
212 muteVideos = mkOption {
215 description = lib.mdDoc "Mute videos by default.";
218 autoplayGifs = mkOption {
221 description = lib.mdDoc "Autoplay GIFs.";
227 description = lib.mdDoc "Instance theme.";
230 infiniteScroll = mkOption {
233 description = lib.mdDoc "Infinite scrolling (requires JavaScript, experimental!).";
236 stickyProfile = mkOption {
239 description = lib.mdDoc "Make profile sidebar stick to top.";
242 bidiSupport = mkOption {
245 description = lib.mdDoc "Support bidirectional text (makes clicking on tweets harder).";
248 hideTweetStats = mkOption {
251 description = lib.mdDoc "Hide tweet stats (replies, retweets, likes).";
254 hideBanner = mkOption {
257 description = lib.mdDoc "Hide profile banner.";
260 hidePins = mkOption {
263 description = lib.mdDoc "Hide pinned tweets.";
266 hideReplies = mkOption {
269 description = lib.mdDoc "Hide tweet replies.";
273 settings = mkOption {
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
284 redisCreateLocally = mkOption {
287 description = lib.mdDoc "Configure local Redis server for Nitter.";
290 openFirewall = mkOption {
293 description = lib.mdDoc "Open ports in the firewall for Nitter web interface.";
298 config = mkIf cfg.enable {
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.";
306 systemd.services.nitter = {
307 description = "Nitter (An alternative Twitter front-end)";
308 wantedBy = [ "multi-user.target" ];
309 after = [ "network.target" ];
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";
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);
333 ProtectControlGroups = 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" ];
350 services.redis.servers.nitter = lib.mkIf (cfg.redisCreateLocally) {
352 port = cfg.cache.redisPort;
355 networking.firewall = mkIf cfg.openFirewall {
356 allowedTCPPorts = [ cfg.server.port ];