{ description = "NixOS configuration for noisebell remote services"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; agenix = { url = "github:ryantm/agenix"; inputs.nixpkgs.follows = "nixpkgs"; }; noisebell-cache.url = "path:./cache-service"; noisebell-discord.url = "path:./discord-bot"; noisebell-rss.url = "path:./rss-service"; noisebell-zulip.url = "path:./zulip-bot"; noisebell-matrix.url = "path:./matrix-bot"; }; outputs = { self, nixpkgs, agenix, noisebell-cache, noisebell-discord, noisebell-rss, noisebell-zulip, noisebell-matrix }: let # ── Cache module ────────────────────────────────────────────────── cacheModule = { config, lib, pkgs, ... }: let cfg = config.services.noisebell-cache; in { options.services.noisebell-cache = { enable = lib.mkEnableOption "noisebell cache service"; domain = lib.mkOption { type = lib.types.str; }; piAddress = lib.mkOption { type = lib.types.str; }; piApiKeyFile = lib.mkOption { type = lib.types.path; description = "Path to agenix secret for authenticating to Pi GET endpoints."; }; inboundApiKeyFile = lib.mkOption { type = lib.types.path; }; port = lib.mkOption { type = lib.types.port; default = 3000; }; statusPollIntervalSecs = lib.mkOption { type = lib.types.ints.positive; default = 60; }; infoPollIntervalSecs = lib.mkOption { type = lib.types.ints.positive; default = 300; }; offlineThreshold = lib.mkOption { type = lib.types.ints.positive; default = 3; }; retryAttempts = lib.mkOption { type = lib.types.ints.unsigned; default = 3; }; retryBaseDelaySecs = lib.mkOption { type = lib.types.ints.positive; default = 1; }; httpTimeoutSecs = lib.mkOption { type = lib.types.ints.positive; default = 10; }; dataDir = lib.mkOption { type = lib.types.str; default = "/var/lib/noisebell-cache"; }; outboundWebhooks = lib.mkOption { type = lib.types.listOf (lib.types.submodule { options = { url = lib.mkOption { type = lib.types.str; }; secretFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; }; }; }); default = []; }; }; config = lib.mkIf cfg.enable { users.users.noisebell-cache = { isSystemUser = true; group = "noisebell-cache"; }; users.groups.noisebell-cache = {}; services.caddy.virtualHosts."${cfg.domain}".extraConfig = '' reverse_proxy localhost:${toString cfg.port} ''; systemd.services.noisebell-cache = let bin = "${noisebell-cache.packages.x86_64-linux.default}/bin/noisebell-cache"; webhookExports = lib.concatImapStringsSep "\n" (i: wh: let idx = toString (i - 1); in ''export NOISEBELL_CACHE_WEBHOOK_${idx}_URL="${wh.url}"'' + lib.optionalString (wh.secretFile != null) ''\nexport NOISEBELL_CACHE_WEBHOOK_${idx}_SECRET="$(cat ${wh.secretFile})"'' ) cfg.outboundWebhooks; in { description = "Noisebell cache service"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; environment = { NOISEBELL_CACHE_PORT = toString cfg.port; NOISEBELL_CACHE_PI_ADDRESS = cfg.piAddress; NOISEBELL_CACHE_DATA_DIR = cfg.dataDir; NOISEBELL_CACHE_STATUS_POLL_INTERVAL_SECS = toString cfg.statusPollIntervalSecs; NOISEBELL_CACHE_INFO_POLL_INTERVAL_SECS = toString cfg.infoPollIntervalSecs; NOISEBELL_CACHE_OFFLINE_THRESHOLD = toString cfg.offlineThreshold; NOISEBELL_CACHE_RETRY_ATTEMPTS = toString cfg.retryAttempts; NOISEBELL_CACHE_RETRY_BASE_DELAY_SECS = toString cfg.retryBaseDelaySecs; NOISEBELL_CACHE_HTTP_TIMEOUT_SECS = toString cfg.httpTimeoutSecs; RUST_LOG = "info"; }; script = '' export NOISEBELL_CACHE_INBOUND_API_KEY="$(cat ${cfg.inboundApiKeyFile})" export NOISEBELL_CACHE_PI_API_KEY="$(cat ${cfg.piApiKeyFile})" ${webhookExports} exec ${bin} ''; serviceConfig = { Type = "simple"; Restart = "on-failure"; RestartSec = 5; User = "noisebell-cache"; Group = "noisebell-cache"; StateDirectory = "noisebell-cache"; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; ReadWritePaths = [ cfg.dataDir ]; }; }; }; }; # ── Discord module ──────────────────────────────────────────────── discordModule = { config, lib, pkgs, ... }: let cfg = config.services.noisebell-discord; in { options.services.noisebell-discord = { enable = lib.mkEnableOption "noisebell Discord bot"; domain = lib.mkOption { type = lib.types.str; }; port = lib.mkOption { type = lib.types.port; default = 3001; }; discordTokenFile = lib.mkOption { type = lib.types.path; }; channelId = lib.mkOption { type = lib.types.str; }; webhookSecretFile = lib.mkOption { type = lib.types.path; }; }; config = lib.mkIf cfg.enable { users.users.noisebell-discord = { isSystemUser = true; group = "noisebell-discord"; }; users.groups.noisebell-discord = {}; services.caddy.virtualHosts."${cfg.domain}".extraConfig = '' reverse_proxy localhost:${toString cfg.port} ''; systemd.services.noisebell-discord = let bin = "${noisebell-discord.packages.x86_64-linux.default}/bin/noisebell-discord"; in { description = "Noisebell Discord bot"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; environment = { NOISEBELL_DISCORD_PORT = toString cfg.port; NOISEBELL_DISCORD_CHANNEL_ID = cfg.channelId; RUST_LOG = "info"; }; script = '' export NOISEBELL_DISCORD_TOKEN="$(cat ${cfg.discordTokenFile})" export NOISEBELL_DISCORD_WEBHOOK_SECRET="$(cat ${cfg.webhookSecretFile})" exec ${bin} ''; serviceConfig = { Type = "simple"; Restart = "on-failure"; RestartSec = 5; User = "noisebell-discord"; Group = "noisebell-discord"; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; }; }; }; }; # ── RSS module ──────────────────────────────────────────────────── rssModule = { config, lib, pkgs, ... }: let cfg = config.services.noisebell-rss; in { options.services.noisebell-rss = { enable = lib.mkEnableOption "noisebell RSS/Atom feed"; domain = lib.mkOption { type = lib.types.str; }; port = lib.mkOption { type = lib.types.port; default = 3002; }; webhookSecretFile = lib.mkOption { type = lib.types.path; }; dataDir = lib.mkOption { type = lib.types.str; default = "/var/lib/noisebell-rss"; }; }; config = lib.mkIf cfg.enable { users.users.noisebell-rss = { isSystemUser = true; group = "noisebell-rss"; }; users.groups.noisebell-rss = {}; services.caddy.virtualHosts."${cfg.domain}".extraConfig = '' reverse_proxy localhost:${toString cfg.port} ''; systemd.services.noisebell-rss = let bin = "${noisebell-rss.packages.x86_64-linux.default}/bin/noisebell-rss"; in { description = "Noisebell RSS/Atom feed"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; environment = { NOISEBELL_RSS_PORT = toString cfg.port; NOISEBELL_RSS_DATA_DIR = cfg.dataDir; NOISEBELL_RSS_SITE_URL = "https://${cfg.domain}"; RUST_LOG = "info"; }; script = '' export NOISEBELL_RSS_WEBHOOK_SECRET="$(cat ${cfg.webhookSecretFile})" exec ${bin} ''; serviceConfig = { Type = "simple"; Restart = "on-failure"; RestartSec = 5; User = "noisebell-rss"; Group = "noisebell-rss"; StateDirectory = "noisebell-rss"; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; ReadWritePaths = [ cfg.dataDir ]; }; }; }; }; # ── Zulip module ────────────────────────────────────────────────── zulipModule = { config, lib, pkgs, ... }: let cfg = config.services.noisebell-zulip; in { options.services.noisebell-zulip = { enable = lib.mkEnableOption "noisebell Zulip bot"; domain = lib.mkOption { type = lib.types.str; }; port = lib.mkOption { type = lib.types.port; default = 3003; }; webhookSecretFile = lib.mkOption { type = lib.types.path; }; serverUrl = lib.mkOption { type = lib.types.str; description = "Zulip server URL (e.g. https://noisebridge.zulipchat.com)"; }; botEmail = lib.mkOption { type = lib.types.str; }; apiKeyFile = lib.mkOption { type = lib.types.path; }; stream = lib.mkOption { type = lib.types.str; default = "general"; }; topic = lib.mkOption { type = lib.types.str; default = "door status"; }; }; config = lib.mkIf cfg.enable { users.users.noisebell-zulip = { isSystemUser = true; group = "noisebell-zulip"; }; users.groups.noisebell-zulip = {}; services.caddy.virtualHosts."${cfg.domain}".extraConfig = '' reverse_proxy localhost:${toString cfg.port} ''; systemd.services.noisebell-zulip = let bin = "${noisebell-zulip.packages.x86_64-linux.default}/bin/noisebell-zulip"; in { description = "Noisebell Zulip bot"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; environment = { NOISEBELL_ZULIP_PORT = toString cfg.port; NOISEBELL_ZULIP_SERVER_URL = cfg.serverUrl; NOISEBELL_ZULIP_BOT_EMAIL = cfg.botEmail; NOISEBELL_ZULIP_STREAM = cfg.stream; NOISEBELL_ZULIP_TOPIC = cfg.topic; RUST_LOG = "info"; }; script = '' export NOISEBELL_ZULIP_WEBHOOK_SECRET="$(cat ${cfg.webhookSecretFile})" export NOISEBELL_ZULIP_API_KEY="$(cat ${cfg.apiKeyFile})" exec ${bin} ''; serviceConfig = { Type = "simple"; Restart = "on-failure"; RestartSec = 5; User = "noisebell-zulip"; Group = "noisebell-zulip"; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; }; }; }; }; # ── Matrix module ───────────────────────────────────────────────── matrixModule = { config, lib, pkgs, ... }: let cfg = config.services.noisebell-matrix; in { options.services.noisebell-matrix = { enable = lib.mkEnableOption "noisebell Matrix bot"; domain = lib.mkOption { type = lib.types.str; }; port = lib.mkOption { type = lib.types.port; default = 3004; }; webhookSecretFile = lib.mkOption { type = lib.types.path; }; homeserver = lib.mkOption { type = lib.types.str; description = "Matrix homeserver URL (e.g. https://matrix.org)"; }; accessTokenFile = lib.mkOption { type = lib.types.path; }; roomId = lib.mkOption { type = lib.types.str; description = "Matrix room ID (e.g. !abc123:matrix.org)"; }; }; config = lib.mkIf cfg.enable { users.users.noisebell-matrix = { isSystemUser = true; group = "noisebell-matrix"; }; users.groups.noisebell-matrix = {}; services.caddy.virtualHosts."${cfg.domain}".extraConfig = '' reverse_proxy localhost:${toString cfg.port} ''; systemd.services.noisebell-matrix = let bin = "${noisebell-matrix.packages.x86_64-linux.default}/bin/noisebell-matrix"; in { description = "Noisebell Matrix bot"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; environment = { NOISEBELL_MATRIX_PORT = toString cfg.port; NOISEBELL_MATRIX_HOMESERVER = cfg.homeserver; NOISEBELL_MATRIX_ROOM_ID = cfg.roomId; RUST_LOG = "info"; }; script = '' export NOISEBELL_MATRIX_WEBHOOK_SECRET="$(cat ${cfg.webhookSecretFile})" export NOISEBELL_MATRIX_ACCESS_TOKEN="$(cat ${cfg.accessTokenFile})" exec ${bin} ''; serviceConfig = { Type = "simple"; Restart = "on-failure"; RestartSec = 5; User = "noisebell-matrix"; Group = "noisebell-matrix"; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; }; }; }; }; in { nixosModules = { cache = cacheModule; discord = discordModule; rss = rssModule; zulip = zulipModule; matrix = matrixModule; }; nixosConfigurations.remote = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ agenix.nixosModules.default cacheModule discordModule rssModule zulipModule matrixModule ./configuration.nix ./hardware-configuration.nix ]; }; }; }