From c5e0a9980b4f92c2a47bf44e507bdb7450037f8c Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Fri, 23 Jul 2021 18:17:25 +0200 Subject: [PATCH 1/2] nitter: init at unstable-2021-07-18 --- pkgs/servers/nitter/default.nix | 131 ++++++++++++++++++++++++++++++++ pkgs/top-level/all-packages.nix | 2 + 2 files changed, 133 insertions(+) create mode 100644 pkgs/servers/nitter/default.nix diff --git a/pkgs/servers/nitter/default.nix b/pkgs/servers/nitter/default.nix new file mode 100644 index 000000000000..47352edf48d3 --- /dev/null +++ b/pkgs/servers/nitter/default.nix @@ -0,0 +1,131 @@ +{ lib +, stdenv +, fetchFromGitHub +, nim +, libsass +}: + +let + jester = fetchFromGitHub { + owner = "dom96"; + repo = "jester"; + rev = "v0.5.0"; + sha256 = "0m8a4ss4460jd2lcbqcbdd68jhcy35xg7qdyr95mh8rflwvmcvhk"; + }; + karax = fetchFromGitHub { + owner = "karaxnim"; + repo = "karax"; + rev = "1.1.2"; + sha256 = "07ykrd21hd76vlmkqpvv5xvaxw6aaq87bky47p2420ni85a6d94j"; + }; + sass = fetchFromGitHub { + owner = "dom96"; + repo = "sass"; + rev = "e683aa1"; + sha256 = "0qvly5rilsqqsyvr67pqhglm55ndc4nd6v90jwswbnigxiqf79lc"; + }; + regex = fetchFromGitHub { + owner = "nitely"; + repo = "nim-regex"; + rev = "2e32fdc"; + sha256 = "1hrl40mwql7nh4wc7sdhmk8bj5728b93v5a93j49v660l0rn4qx8"; + }; + unicodedb = fetchFromGitHub { + owner = "nitely"; + repo = "nim-unicodedb"; + rev = "v0.9.0"; + sha256 = "06j8d0bjbpv1iibqlmrac4qb61ggv17hvh6nv4pbccqk1rlpxhsq"; + }; + unicodeplus= fetchFromGitHub { + owner = "nitely"; + repo = "nim-unicodeplus"; + rev = "v0.8.0"; + sha256 = "181wzwivfgplkqn5r4crhnaqgsza7x6fi23i86djb2dxvm7v6qxk"; + }; + segmentation = fetchFromGitHub { + owner = "nitely"; + repo = "nim-segmentation"; + rev = "v0.1.0"; + sha256 = "007bkx8dwy8n340zbp6wyqfsq9bh6q5ykav1ywdlwykyp1n909bh"; + }; + nimcrypto = fetchFromGitHub { + owner = "cheatfate"; + repo = "nimcrypto"; + rev = "a5742a9a214ac33f91615f3862c7b099aec43b00"; + sha256 = "0al0jsaicm8vyr63n909dq1glhvpra1n9sllmj0r7lsjsdb59wsz"; + }; + markdown = fetchFromGitHub { + owner = "soasme"; + repo = "nim-markdown"; + rev = "abdbe5e"; + sha256 = "0f3c1sxvhbbds43c9l8cz69pfpf984msj1lv4pb7bzpxb5zil2wy"; + }; + packedjson = fetchFromGitHub { + owner = "Araq"; + repo = "packedjson"; + rev = "7198cc8"; + sha256 = "1ay2zd88q8hvpvigsg8h0y5vc65hk3lk0d48fy9hwg4lcng19mp1"; + }; + supersnappy = fetchFromGitHub { + owner = "guzba"; + repo = "supersnappy"; + rev = "1.1.5"; + sha256 = "1y26sgnszvdf5sn7j0jx2dpd4i03mvbk9i9ni9kbyrs798bjwi6z"; + }; + redpool = fetchFromGitHub { + owner = "zedeus"; + repo = "redpool"; + rev = "57aeb25"; + sha256 = "0fph7qlia6fvya1zqzbgvww2hk5pd0vq1wlf9ij9jyn655mg0w3q"; + }; + frosty = fetchFromGitHub { + owner = "disruptek"; + repo = "frosty"; + rev = "0.3.1"; + sha256 = "0hd6484ihjgl57gmqyp5xfq5prycb49k0313fqky600mhz71nmyz"; + }; + redis = fetchFromGitHub { + owner = "zedeus"; + repo = "redis"; + rev = "94bcbf1"; + sha256 = "1p9zv4f4lqrjqa8fk98cb89b9fzlf866jc584ll9sws14904i80j"; + }; +in stdenv.mkDerivation rec { + pname = "nitter"; + version = "unstable-2021-07-18"; + + src = fetchFromGitHub { + owner = "zedeus"; + repo = "nitter"; + rev = "6c5cb01b294d4f6e3b438fc47683359eb0fe5057"; + sha256 = "1dl8ndyv8m1hnydrp5xilcpp2cfbp02d5jap3y42i4nazc9ar6p4"; + }; + + nativeBuildInputs = [ nim ]; + buildInputs = [ libsass ]; + + buildPhase = '' + runHook preBuild + export HOME=$TMPDIR + nim -d:release -p:${jester} -p:${karax} -p:${sass}/src -p:${regex}/src -p:${unicodedb}/src -p:${unicodeplus}/src -p:${segmentation}/src -p:${nimcrypto} -p:${markdown}/src -p:${packedjson} -p:${supersnappy}/src -p:${redpool}/src -p:${frosty} -p:${redis}/src c src/$pname + nim -p:${sass}/src c --hint[Processing]:off -r tools/gencss + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + install -Dt $out/bin src/$pname + mkdir -p $out/share/nitter + cp -r public $out/share/nitter/public + runHook postInstall + ''; + + meta = with lib; { + description = "Alternative Twitter front-end"; + homepage = "https://github.com/zedeus/nitter"; + maintainers = with maintainers; [ erdnaxe ]; + license = licenses.agpl3Only; + platforms = [ "x86_64-linux" ]; + }; +} + diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 827a156f7f2b..73f4bde028ee 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -7363,6 +7363,8 @@ in ngrok-1 = callPackage ../tools/networking/ngrok-1 { }; + nitter = callPackage ../servers/nitter { }; + noice = callPackage ../applications/misc/noice { }; noip = callPackage ../tools/networking/noip { }; From 534dbcb28f654c770a6e226e66c4299182494904 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Mon, 26 Jul 2021 12:00:05 +0200 Subject: [PATCH 2/2] nixos/nitter: init module and test --- nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/nitter.nix | 326 +++++++++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/nitter.nix | 16 ++ 4 files changed, 344 insertions(+) create mode 100644 nixos/modules/services/misc/nitter.nix create mode 100644 nixos/tests/nitter.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 4d1700ed99af..27f0456c11eb 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -530,6 +530,7 @@ ./services/misc/metabase.nix ./services/misc/mwlib.nix ./services/misc/n8n.nix + ./services/misc/nitter.nix ./services/misc/nix-daemon.nix ./services/misc/nix-gc.nix ./services/misc/nix-optimise.nix diff --git a/nixos/modules/services/misc/nitter.nix b/nixos/modules/services/misc/nitter.nix new file mode 100644 index 000000000000..095a15f21f6a --- /dev/null +++ b/nixos/modules/services/misc/nitter.nix @@ -0,0 +1,326 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.nitter; + configFile = pkgs.writeText "nitter.conf" '' + ${generators.toINI { + # String values need to be quoted + mkKeyValue = generators.mkKeyValueDefault { + mkValueString = v: + if isString v then "\"" + (strings.escape ["\""] (toString v)) + "\"" + else generators.mkValueStringDefault {} v; + } " = "; + } (lib.recursiveUpdate { + Server = cfg.server; + Cache = cfg.cache; + Config = cfg.config // { hmacKey = "@hmac@"; }; + Preferences = cfg.preferences; + } cfg.settings)} + ''; + # `hmac` is a secret used for cryptographic signing of video URLs. + # Generate it on first launch, then copy configuration and replace + # `@hmac@` with this value. + # We are not using sed as it would leak the value in the command line. + preStart = pkgs.writers.writePython3 "nitter-prestart" {} '' + import os + import secrets + + state_dir = os.environ.get("STATE_DIRECTORY") + if not os.path.isfile(f"{state_dir}/hmac"): + # Generate hmac on first launch + hmac = secrets.token_hex(32) + with open(f"{state_dir}/hmac", "w") as f: + f.write(hmac) + else: + # Load previously generated hmac + with open(f"{state_dir}/hmac", "r") as f: + hmac = f.read() + + configFile = "${configFile}" + with open(configFile, "r") as f_in: + with open(f"{state_dir}/nitter.conf", "w") as f_out: + f_out.write(f_in.read().replace("@hmac@", hmac)) + ''; +in +{ + options = { + services.nitter = { + enable = mkEnableOption "If enabled, start Nitter."; + + server = { + address = mkOption { + type = types.str; + default = "0.0.0.0"; + example = "127.0.0.1"; + description = "The address to listen on."; + }; + + port = mkOption { + type = types.port; + default = 8080; + example = 8000; + description = "The port to listen on."; + }; + + https = mkOption { + type = types.bool; + default = false; + description = "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS."; + }; + + httpMaxConnections = mkOption { + type = types.int; + default = 100; + description = "Maximum number of HTTP connections."; + }; + + staticDir = mkOption { + type = types.path; + default = "${pkgs.nitter}/share/nitter/public"; + defaultText = "\${pkgs.nitter}/share/nitter/public"; + description = "Path to the static files directory."; + }; + + title = mkOption { + type = types.str; + default = "nitter"; + description = "Title of the instance."; + }; + + hostname = mkOption { + type = types.str; + default = "localhost"; + example = "nitter.net"; + description = "Hostname of the instance."; + }; + }; + + cache = { + listMinutes = mkOption { + type = types.int; + default = 240; + description = "How long to cache list info (not the tweets, so keep it high)."; + }; + + rssMinutes = mkOption { + type = types.int; + default = 10; + description = "How long to cache RSS queries."; + }; + + redisHost = mkOption { + type = types.str; + default = "localhost"; + description = "Redis host."; + }; + + redisPort = mkOption { + type = types.port; + default = 6379; + description = "Redis port."; + }; + + redisConnections = mkOption { + type = types.int; + default = 20; + description = "Redis connection pool size."; + }; + + redisMaxConnections = mkOption { + type = types.int; + default = 30; + description = '' + Maximum number of connections to Redis. + + New connections are opened when none are available, but if the + pool size goes above this, they are closed when released, do not + worry about this unless you receive tons of requests per second. + ''; + }; + }; + + config = { + base64Media = mkOption { + type = types.bool; + default = false; + description = "Use base64 encoding for proxied media URLs."; + }; + + tokenCount = mkOption { + type = types.int; + default = 10; + description = '' + Minimum amount of usable tokens. + + Tokens are used to authorize API requests, but they expire after + ~1 hour, and have a limit of 187 requests. The limit gets reset + every 15 minutes, and the pool is filled up so there is always at + least tokenCount usable tokens. Only increase this if you receive + major bursts all the time. + ''; + }; + }; + + preferences = { + replaceTwitter = mkOption { + type = types.str; + default = ""; + example = "nitter.net"; + description = "Replace Twitter links with links to this instance (blank to disable)."; + }; + + replaceYouTube = mkOption { + type = types.str; + default = ""; + example = "piped.kavin.rocks"; + description = "Replace YouTube links with links to this instance (blank to disable)."; + }; + + replaceInstagram = mkOption { + type = types.str; + default = ""; + description = "Replace Instagram links with links to this instance (blank to disable)."; + }; + + mp4Playback = mkOption { + type = types.bool; + default = true; + description = "Enable MP4 video playback."; + }; + + hlsPlayback = mkOption { + type = types.bool; + default = false; + description = "Enable HLS video streaming (requires JavaScript)."; + }; + + proxyVideos = mkOption { + type = types.bool; + default = true; + description = "Proxy video streaming through the server (might be slow)."; + }; + + muteVideos = mkOption { + type = types.bool; + default = false; + description = "Mute videos by default."; + }; + + autoplayGifs = mkOption { + type = types.bool; + default = true; + description = "Autoplay GIFs."; + }; + + theme = mkOption { + type = types.str; + default = "Nitter"; + description = "Instance theme."; + }; + + infiniteScroll = mkOption { + type = types.bool; + default = false; + description = "Infinite scrolling (requires JavaScript, experimental!)."; + }; + + stickyProfile = mkOption { + type = types.bool; + default = true; + description = "Make profile sidebar stick to top."; + }; + + bidiSupport = mkOption { + type = types.bool; + default = false; + description = "Support bidirectional text (makes clicking on tweets harder)."; + }; + + hideTweetStats = mkOption { + type = types.bool; + default = false; + description = "Hide tweet stats (replies, retweets, likes)."; + }; + + hideBanner = mkOption { + type = types.bool; + default = false; + description = "Hide profile banner."; + }; + + hidePins = mkOption { + type = types.bool; + default = false; + description = "Hide pinned tweets."; + }; + + hideReplies = mkOption { + type = types.bool; + default = false; + description = "Hide tweet replies."; + }; + }; + + settings = mkOption { + type = types.attrs; + default = {}; + description = '' + Add settings here to override NixOS module generated settings. + + Check the official repository for the available settings: + https://github.com/zedeus/nitter/blob/master/nitter.conf + ''; + }; + + redisCreateLocally = mkOption { + type = types.bool; + default = true; + description = "Configure local Redis server for Nitter."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open ports in the firewall for Nitter web interface."; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = !cfg.redisCreateLocally || (cfg.cache.redisHost == "localhost" && cfg.cache.redisPort == 6379); + message = "When services.nitter.redisCreateLocally is enabled, you need to use localhost:6379 as a cache server."; + } + ]; + + systemd.services.nitter = { + description = "Nitter (An alternative Twitter front-end)"; + wantedBy = [ "multi-user.target" ]; + after = [ "syslog.target" "network.target" ]; + serviceConfig = { + DynamicUser = true; + StateDirectory = "nitter"; + Environment = [ "NITTER_CONF_FILE=/var/lib/nitter/nitter.conf" ]; + # Some parts of Nitter expect `public` folder in working directory, + # see https://github.com/zedeus/nitter/issues/414 + WorkingDirectory = "${pkgs.nitter}/share/nitter"; + ExecStart = "${pkgs.nitter}/bin/nitter"; + ExecStartPre = "${preStart}"; + AmbientCapabilities = lib.mkIf (cfg.server.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; + Restart = "on-failure"; + RestartSec = "5s"; + }; + }; + + services.redis = lib.mkIf (cfg.redisCreateLocally) { + enable = true; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.server.port ]; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index d6ef7d42431f..87dfe14bb977 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -297,6 +297,7 @@ in nginx-sandbox = handleTestOn ["x86_64-linux"] ./nginx-sandbox.nix {}; nginx-sso = handleTest ./nginx-sso.nix {}; nginx-variants = handleTest ./nginx-variants.nix {}; + nitter = handleTest ./nitter.nix {}; nix-serve = handleTest ./nix-ssh-serve.nix {}; nix-ssh-serve = handleTest ./nix-ssh-serve.nix {}; nixos-generate-config = handleTest ./nixos-generate-config.nix {}; diff --git a/nixos/tests/nitter.nix b/nixos/tests/nitter.nix new file mode 100644 index 000000000000..e17f1c473436 --- /dev/null +++ b/nixos/tests/nitter.nix @@ -0,0 +1,16 @@ +import ./make-test-python.nix ({ pkgs, ... }: + +{ + name = "nitter"; + meta.maintainers = with pkgs.lib.maintainers; [ erdnaxe ]; + + nodes.machine = { + services.nitter.enable = true; + }; + + testScript = '' + machine.wait_for_unit("nitter.service") + machine.wait_for_open_port("8080") + machine.succeed("curl --fail http://localhost:8080/") + ''; +})