2 Commits

Author SHA1 Message Date
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
finn.markwitz 70deb25080 chore: Flake Update 2026-08-21 10:30:01 +02:00
6 changed files with 120 additions and 18 deletions
Generated
+14 -14
View File
@@ -89,11 +89,11 @@
"nixpkgs-regression": "nixpkgs-regression" "nixpkgs-regression": "nixpkgs-regression"
}, },
"locked": { "locked": {
"lastModified": 1786447342, "lastModified": 1787274306,
"narHash": "sha256-jA79fFQUEsLcpmjWRrlOgVVFVKUQSD4zOZIRKEpEx88=", "narHash": "sha256-Qg9f9td5iphUWSQS6zmvyZWO1F+D7j8Z3U6dGyUTg08=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nix", "repo": "nix",
"rev": "da110e01d913bb32bdc06a301c3797e8c42b8ab7", "rev": "649e823fb24ed118d72e613be35fa8ea1b64afe7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -143,11 +143,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1786437054, "lastModified": 1787144466,
"narHash": "sha256-I++HzBBAgQ17UaLVU6aSm1/7LDo6c9xr8rAbpByWywE=", "narHash": "sha256-HHfv2/HkNSKbbSyU9iD/g8lbP6r4tl33sSw1W4rXCk0=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixos-hardware", "repo": "nixos-hardware",
"rev": "6ed13b1d888d5cb07dbb0723eb1df86bbacd0b9c", "rev": "0471accf8d0a8210b31d947497d179ecc99e0021",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -218,11 +218,11 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1786247143, "lastModified": 1787135253,
"narHash": "sha256-qemNveuexlWhK0Qc6Y8o+gtSVTIj5xljzTAXQZxTHWA=", "narHash": "sha256-M5/r2v++FjVhdsxXYMb4BDJ5YLAdCWFt3aZotcshocA=",
"rev": "279b4a8275f032c566576b3f181fa0f27197f588", "rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1050399.279b4a8275f0/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1058091.ffb3c9b700e7/nixexprs.tar.xz"
}, },
"original": { "original": {
"id": "nixpkgs", "id": "nixpkgs",
@@ -245,11 +245,11 @@
}, },
"nixpkgs_3": { "nixpkgs_3": {
"locked": { "locked": {
"lastModified": 1786313170, "lastModified": 1787101114,
"narHash": "sha256-F5163SsL9xqCLcx8CS2Y5MeLA5z6mfgXGDiCBEqSnbo=", "narHash": "sha256-BA7sSNjLDuPGSOYBGpr6WQjke1MQ8AZpJ8GlYZM/mOc=",
"rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d", "rev": "b18a4b905f8d028dc4476412e6d6891728695379",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.7376.fcb8fcd6bf2d/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.8045.b18a4b905f8d/nixexprs.tar.xz"
}, },
"original": { "original": {
"id": "nixpkgs", "id": "nixpkgs",
+1
View File
@@ -22,6 +22,7 @@ in
jellyfin.enable = true; jellyfin.enable = true;
jellyseerr.enable = true; jellyseerr.enable = true;
immich.enable = true; immich.enable = true;
newsreader.enable = true;
development.enable = true; development.enable = true;
home-assistant.enable = true; home-assistant.enable = true;
+1 -1
View File
@@ -4,7 +4,7 @@ _: {
]; ];
networking = { networking = {
domain = "jupiter"; domain = "jupiter.solar.internal";
search = [ "jupiter.solar.internal" ]; search = [ "jupiter.solar.internal" ];
}; };
} }
+1
View File
@@ -20,5 +20,6 @@
./jellyfin ./jellyfin
./jellyseerr ./jellyseerr
./immich ./immich
./newsreader
]; ];
} }
+1 -3
View File
@@ -91,12 +91,10 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [ dashboardPort ];
services.homepage-dashboard = { services.homepage-dashboard = {
enable = true; enable = true;
listenPort = dashboardPort; listenPort = dashboardPort;
allowedHosts = "${dashboardHost}:${toString dashboardPort},192.168.178.65:${toString dashboardPort},localhost:${toString dashboardPort},127.0.0.1:${toString dashboardPort},jupiter:${toString dashboardPort},jupiter.solar.internal:${toString dashboardPort}"; allowedHosts = "${dashboardHost}:${toString dashboardPort},localhost:${toString dashboardPort},127.0.0.1:${toString dashboardPort},jupiter.solar.internal:${toString dashboardPort}";
bookmarks = import ./bookmarks.nix; bookmarks = import ./bookmarks.nix;
services = homepageServices ++ manualServices; services = homepageServices ++ manualServices;
widgets = config.my.homepage.widgets ++ manualWidgets; widgets = config.my.homepage.widgets ++ manualWidgets;
+102
View File
@@ -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=<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";
}
];
};
}