diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 1f9e5f780491..3c17c6f3d764 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -110,6 +110,8 @@ - [turborepo-remote-cache](https://ducktors.github.io/turborepo-remote-cache/), an open-source implementation of the [Turborepo custom remote cache server](https://turbo.build/repo/docs/core-concepts/remote-caching#self-hosting). Available as [services.turborepo-remote-cache](options.html#opt-services.turborepo-remote-cache). +- [RSSHub](https://github.com/DIYgod/RSSHub), a service to convert many sources into rss. Available as `services.rsshub`. + - [Komodo Periphery](https://github.com/moghtech/komodo), a multi-server Docker and Git deployment agent by Komodo. Available as [services.komodo-periphery](#opt-services.komodo-periphery.enable). - [Shoko](https://shokoanime.com), an anime management system. Available as [services.shoko](#opt-services.shoko.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index ce93b9560173..b23517fcce16 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1760,6 +1760,7 @@ ./services/web-apps/reposilite.nix ./services/web-apps/rimgo.nix ./services/web-apps/rss-bridge.nix + ./services/web-apps/rsshub.nix ./services/web-apps/rutorrent.nix ./services/web-apps/screego.nix ./services/web-apps/selfoss.nix diff --git a/nixos/modules/services/web-apps/rsshub.nix b/nixos/modules/services/web-apps/rsshub.nix new file mode 100644 index 000000000000..912a6d36c1d3 --- /dev/null +++ b/nixos/modules/services/web-apps/rsshub.nix @@ -0,0 +1,138 @@ +{ + lib, + config, + pkgs, + ... +}: + +let + cfg = config.services.rsshub; +in +{ + options.services.rsshub = { + enable = lib.mkEnableOption "RSSHub service"; + + package = lib.mkPackageOption pkgs "rsshub" { }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to open the firewall for the specified port."; + }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = lib.types.attrsOf lib.types.str; + options = { + LISTEN_INADDR_ANY = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Listen to any address"; + apply = x: if x then "1" else "0"; + }; + PORT = lib.mkOption { + type = lib.types.port; + default = 1200; + description = "Listen on port."; + apply = toString; + }; + NO_LOGFILES = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Print logs into stderr."; + apply = x: if x then "1" else "0"; + }; + }; + }; + default = { }; + example = lib.literalExpression '' + { + REQUEST_TIMEOUT = "3000"; + REQUEST_RETRY = "10"; + PUPPETEER_EXECUTABLE_PATH = lib.getExe pkgs.chromium"; + } + ''; + description = '' + Environment variables for RSSHub. + See for available options. + ''; + }; + + secretFiles = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = [ ]; + example = lib.literalExpression '' + [ config.sops.secrets.rsshub.path ] + ''; + description = '' + Environment variables stored in files for secrets. + See for available options. + ''; + }; + + redis = { + enable = lib.mkEnableOption "Redis for RSSHub"; + createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Create and use a local Redis instance. Sets `services.redis.servers.rsshub`."; + }; + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = "The Redis host."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 6379; + description = "The Redis port."; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.redis.servers.rsshub = lib.mkIf (cfg.redis.enable && cfg.redis.createLocally) { + enable = true; + port = cfg.redis.port; + }; + + services.rsshub.settings = lib.mkIf cfg.redis.enable { + CACHE_TYPE = "redis"; + REDIS_URL = "redis://${cfg.redis.host}:${toString cfg.redis.port}"; + }; + + systemd.services.rsshub = { + description = "RSSHub - Everything is RSSible"; + wantedBy = [ "multi-user.target" ]; + after = lib.optional (cfg.redis.enable && cfg.redis.createLocally) "redis-rsshub.service"; + requires = lib.optional (cfg.redis.enable && cfg.redis.createLocally) "redis-rsshub.service"; + + environment = cfg.settings; + + serviceConfig = { + Type = "simple"; + User = "rsshub"; + Group = "rsshub"; + DynamicUser = true; + StateDirectory = "rsshub"; + EnvironmentFile = cfg.secretFiles; + ExecStart = lib.getExe cfg.package; + Restart = "on-failure"; + RestartSec = "10s"; + + # Hardening + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ (lib.toInt cfg.settings.PORT) ]; + }; + + meta.maintainers = with lib.maintainers; [ vonfry ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index b6512c99ca01..929e8362e8f3 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1432,6 +1432,7 @@ in rspamd-trainer = runTest ./rspamd-trainer.nix; rss-bridge = handleTest ./web-apps/rss-bridge { }; rss2email = handleTest ./rss2email.nix { }; + rsshub = runTest ./web-apps/rsshub.nix; rstp = runTest ./rstp.nix; rstudio-server = runTest ./rstudio-server.nix; rsync = runTest ./rsync.nix; diff --git a/nixos/tests/web-apps/rsshub.nix b/nixos/tests/web-apps/rsshub.nix new file mode 100644 index 000000000000..89557aa2ab01 --- /dev/null +++ b/nixos/tests/web-apps/rsshub.nix @@ -0,0 +1,46 @@ +{ lib, ... }: +{ + name = "rsshub"; + + meta.maintainers = with lib.maintainers; [ vonfry ]; + + nodes = { + basic = { + services.rsshub.enable = true; + }; + + redis = { + services.rsshub = { + enable = true; + + redis = { + enable = true; + createLocally = true; + }; + }; + }; + }; + + testScript = + { nodes, ... }: + let + port = nodes.basic.services.rsshub.settings.PORT; + in + '' + def assert_http_200(machine, url): + code = machine.succeed(f"curl -s -o /dev/null -w '%{{http_code}}' {url}").strip() + assert code == "200", f"Expected HTTP 200 from {url}, got {code}" + + def test_node(node): + node.wait_for_unit("rsshub.service") + node.wait_for_open_port(${port}) + assert_http_200(node, "http://localhost:${port}/healthz") + + with subtest("RSSHub works"): + test_node(basic) + + with subtest("RSSHub works with local Redis"): + test_node(redis) + + ''; +}