From b7ae8cfec2379993f3c7092de3c309f598364a2d Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Sun, 23 Aug 2026 10:21:09 +0200 Subject: [PATCH] feat(jupiter): add newsreader profile (RSSHub + Miniflux) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X killed guest API access and now prices reads at $0.005 each with no free tier, so the only workable personal news reader is a scraping bridge fed by a burner account's session cookie. RSSHub turns X accounts, lists and keyword searches into RSS on loopback; Miniflux is the reader, exposed on the LAN and registered with homepage. Both secrets live outside the Nix store as root-owned EnvironmentFiles. Note: the packaged RSSHub reads TWITTER_AUTH_TOKEN — the username/password login that upstream docs still describe is commented out in that build. Co-Authored-By: Claude Opus 5 --- machines/jupiter/environments.nix | 1 + modules/environments/default.nix | 1 + modules/environments/newsreader/default.nix | 102 ++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 modules/environments/newsreader/default.nix diff --git a/machines/jupiter/environments.nix b/machines/jupiter/environments.nix index e36c825..7d54c16 100644 --- a/machines/jupiter/environments.nix +++ b/machines/jupiter/environments.nix @@ -22,6 +22,7 @@ in jellyfin.enable = true; jellyseerr.enable = true; immich.enable = true; + newsreader.enable = true; development.enable = true; home-assistant.enable = true; diff --git a/modules/environments/default.nix b/modules/environments/default.nix index 9158f6a..1c398c8 100644 --- a/modules/environments/default.nix +++ b/modules/environments/default.nix @@ -20,5 +20,6 @@ ./jellyfin ./jellyseerr ./immich + ./newsreader ]; } diff --git a/modules/environments/newsreader/default.nix b/modules/environments/newsreader/default.nix new file mode 100644 index 0000000..9678c24 --- /dev/null +++ b/modules/environments/newsreader/default.nix @@ -0,0 +1,102 @@ +# X (Twitter) news reader: RSSHub feed bridge + Miniflux reader +{ + config, + lib, + ... +}: +let + cfg = config.my.profiles.newsreader; + hostName = config.networking.hostName; + + # RSSHub only ever talks to Miniflux on the same host, so it stays on + # loopback and out of the firewall. + rsshubPort = 1200; +in +{ + options.my.profiles.newsreader = with lib; { + enable = mkEnableOption "RSSHub + Miniflux news reader"; + + port = mkOption { + type = types.port; + default = 8085; # 8080 is taken by aria on jupiter + description = "Port Miniflux listens on."; + }; + + rsshubSecretFile = mkOption { + type = types.path; + default = "/var/lib/secrets/rsshub.env"; + description = '' + EnvironmentFile holding RSSHub's X session, in the form + + ``` + TWITTER_AUTH_TOKEN=, + ``` + + X removed guest access, so the bridge needs a logged-in session: copy + the `auth_token` cookie from a burner account and close the tab without + logging out, since logging out invalidates it. Listing several cookies + gives RSSHub rotation headroom when one gets suspended. + + Create this file by hand, root-owned and chmod 600 — it must not end up + in the Nix store. + ''; + }; + + minifluxSecretFile = mkOption { + type = types.path; + default = "/var/lib/secrets/miniflux.env"; + description = '' + EnvironmentFile holding the Miniflux admin account: + + ``` + ADMIN_USERNAME=finn + ADMIN_PASSWORD= + ``` + ''; + }; + }; + + config = lib.mkIf cfg.enable { + # Turns X accounts, lists and keyword searches into RSS. Feed URLs look + # like http://127.0.0.1:1200/twitter/user/, /twitter/list/ or + # /twitter/keyword/. + services.rsshub = { + enable = true; + redis.enable = true; + secretFiles = [ cfg.rsshubSecretFile ]; + settings = { + PORT = rsshubPort; + LISTEN_INADDR_ANY = false; + # X throttles aggressively and answers with an empty 200 rather than an + # error, so cache for an hour and keep retries low. + CACHE_EXPIRE = "3600"; + REQUEST_RETRY = "3"; + }; + }; + + services.miniflux = { + enable = true; + adminCredentialsFile = cfg.minifluxSecretFile; + config = { + LISTEN_ADDR = "0.0.0.0:${toString cfg.port}"; + BASE_URL = "http://${hostName}:${toString cfg.port}/"; + CREATE_ADMIN = 1; + # Minutes. Matched to RSSHub's cache; polling harder just burns the + # X session for nothing. + POLLING_FREQUENCY = 60; + }; + }; + + networking.firewall.allowedTCPPorts = [ cfg.port ]; + + my.homepage.services = [ + { + group = "Services"; + name = "Miniflux"; + description = "RSS reader"; + href = "http://${hostName}:${toString cfg.port}"; + icon = "miniflux.png"; + } + ]; + }; +}