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"; + } + ]; + }; +}