b7ae8cfec2
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>
103 lines
2.9 KiB
Nix
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";
|
|
}
|
|
];
|
|
};
|
|
}
|