Files
nixos/modules/environments/newsreader/default.nix
T
finn.markwitz b7ae8cfec2 feat(jupiter): add newsreader profile (RSSHub + Miniflux)
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 <noreply@anthropic.com>
2026-08-23 10:21:09 +02:00

103 lines
2.9 KiB
Nix

# 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=<auth_token cookie>,<optional second cookie>
```
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=<at least 6 characters>
```
'';
};
};
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/<handle>, /twitter/list/<id> or
# /twitter/keyword/<query>.
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";
}
];
};
}