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