From c5e0a9980b4f92c2a47bf44e507bdb7450037f8c Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
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 <erdnaxe@crans.org>
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/")
+  '';
+})