Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379{ pkgs, config, lib, ...}: let cfg = config.services.tangled.appview;in with lib; { options = { services.tangled.appview = { enable = mkOption { type = types.bool; default = false; description = "Enable tangled appview"; };
package = mkOption { type = types.package; description = "Package to use for the appview"; };
# core configuration port = mkOption { type = types.port; default = 3000; description = "Port to run the appview on"; };
listenAddr = mkOption { type = types.str; default = "0.0.0.0:${toString cfg.port}"; description = "Listen address for the appview service"; };
metricsListenAddr = mkOption { type = types.str; default = "0.0.0.0:9090"; description = "Listen address for the Prometheus metrics endpoint"; };
dbPath = mkOption { type = types.str; default = "/var/lib/appview/appview.db"; description = "Path to the SQLite database file"; };
appviewHost = mkOption { type = types.str; default = "tangled.org"; example = "example.com"; description = "Public host URL for the appview instance"; };
appviewName = mkOption { type = types.str; default = "Tangled"; description = "Display name for the appview instance"; };
dev = mkOption { type = types.bool; default = false; description = "Enable development mode"; };
disallowedNicknamesFile = mkOption { type = types.nullOr types.path; default = null; description = "Path to file containing disallowed nicknames"; };
# redis configuration redis = { addr = mkOption { type = types.str; default = "localhost:6379"; description = "Redis server address"; };
db = mkOption { type = types.int; default = 0; description = "Redis database number"; }; };
# jetstream configuration jetstream = { endpoint = mkOption { type = types.str; default = "wss://jetstream1.us-east.bsky.network/subscribe"; description = "Jetstream WebSocket endpoint"; }; };
# knotstream consumer configuration knotstream = { retryInterval = mkOption { type = types.str; default = "60s"; description = "Initial retry interval for knotstream consumer"; };
maxRetryInterval = mkOption { type = types.str; default = "120m"; description = "Maximum retry interval for knotstream consumer"; };
connectionTimeout = mkOption { type = types.str; default = "5s"; description = "Connection timeout for knotstream consumer"; };
workerCount = mkOption { type = types.int; default = 64; description = "Number of workers for knotstream consumer"; };
queueSize = mkOption { type = types.int; default = 100; description = "Queue size for knotstream consumer"; }; };
# spindlestream consumer configuration spindlestream = { retryInterval = mkOption { type = types.str; default = "60s"; description = "Initial retry interval for spindlestream consumer"; };
maxRetryInterval = mkOption { type = types.str; default = "120m"; description = "Maximum retry interval for spindlestream consumer"; };
connectionTimeout = mkOption { type = types.str; default = "5s"; description = "Connection timeout for spindlestream consumer"; };
workerCount = mkOption { type = types.int; default = 64; description = "Number of workers for spindlestream consumer"; };
queueSize = mkOption { type = types.int; default = 100; description = "Queue size for spindlestream consumer"; }; };
# resend configuration resend = { sentFrom = mkOption { type = types.str; default = "noreply@notifs.tangled.sh"; description = "Email address to send notifications from"; }; };
# posthog configuration posthog = { endpoint = mkOption { type = types.str; default = "https://eu.i.posthog.com"; description = "PostHog API endpoint"; }; };
# camo configuration camo = { host = mkOption { type = types.str; default = "https://camo.tangled.sh"; description = "Camo proxy host URL"; }; };
# avatar configuration avatar = { host = mkOption { type = types.str; default = "https://avatar.tangled.sh"; description = "Avatar service host URL"; }; };
plc = { url = mkOption { type = types.str; default = "https://plc.directory"; description = "PLC directory URL"; }; };
pds = { host = mkOption { type = types.str; default = "https://tngl.sh"; description = "PDS host URL"; }; };
label = { defaults = mkOption { type = types.listOf types.str; default = [ "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/wontfix" "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue" "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/duplicate" "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/documentation" "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/assignee" ]; description = "Default label definitions"; };
goodFirstIssue = mkOption { type = types.str; default = "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue"; description = "Good first issue label definition"; }; };
# ssh log server configuration ssh = { enable = mkOption { type = types.bool; default = false; description = "Enable the SSH pipeline log server"; };
listenAddr = mkOption { type = types.str; default = "0.0.0.0:3333"; description = "Listen address for the SSH log server"; };
hostKeyPath = mkOption { type = types.nullOr types.str; default = null; example = "/var/lib/appview/ssh_host_key"; description = '' Path to the SSH host key file. If null, an ephemeral key is generated on each startup. generate with:
ssh-keygen -t ed25519 -N "" -f /var/lib/appview/ssh_host_key ''; }; };
environmentFile = mkOption { type = with types; nullOr path; default = null; example = "/etc/appview.env"; description = '' Additional environment file as defined in {manpage}`systemd.exec(5)`.
Sensitive secrets such as {env}`TANGLED_COOKIE_SECRET`, {env}`TANGLED_OAUTH_CLIENT_SECRET`, {env}`TANGLED_RESEND_API_KEY`, {env}`TANGLED_CAMO_SHARED_SECRET`, {env}`TANGLED_AVATAR_SHARED_SECRET`, {env}`TANGLED_REDIS_PASS`, {env}`TANGLED_PDS_ADMIN_SECRET`, {env}`TANGLED_KNOT_ADMIN_SECRET`, {env}`TANGLED_CLOUDFLARE_API_TOKEN`, {env}`TANGLED_CLOUDFLARE_ZONE_ID`, {env}`TANGLED_CLOUDFLARE_TURNSTILE_SITE_KEY`, {env}`TANGLED_CLOUDFLARE_TURNSTILE_SECRET_KEY`, {env}`TANGLED_POSTHOG_API_KEY`, {env}`TANGLED_APP_PASSWORD`, and {env}`TANGLED_ALT_APP_PASSWORD` may be passed to the service without making them world readable in the nix store. ''; }; }; };
config = mkIf cfg.enable { services.redis.servers.appview = { enable = true; port = 6379; };
systemd.services.appview = { description = "tangled appview service"; wantedBy = ["multi-user.target"]; after = ["redis-appview.service" "network-online.target"]; requires = ["redis-appview.service"]; wants = ["network-online.target"];
path = [pkgs.diffutils];
serviceConfig = { Type = "simple"; ExecStart = "${cfg.package}/bin/appview"; Restart = "always"; RestartSec = "10s"; EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
# state directory StateDirectory = "appview"; WorkingDirectory = "/var/lib/appview";
# security hardening NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = ["/var/lib/appview"] ++ optionals (cfg.ssh.enable && cfg.ssh.hostKeyPath != null) [cfg.ssh.hostKeyPath]; };
environment = { TANGLED_DB_PATH = cfg.dbPath; TANGLED_LISTEN_ADDR = cfg.listenAddr; TANGLED_METRICS_LISTEN_ADDR = cfg.metricsListenAddr; TANGLED_APPVIEW_HOST = cfg.appviewHost; TANGLED_APPVIEW_NAME = cfg.appviewName; TANGLED_DEV = if cfg.dev then "true" else "false"; } // optionalAttrs (cfg.disallowedNicknamesFile != null) { TANGLED_DISALLOWED_NICKNAMES_FILE = cfg.disallowedNicknamesFile; } // { TANGLED_REDIS_ADDR = cfg.redis.addr; TANGLED_REDIS_DB = toString cfg.redis.db;
TANGLED_JETSTREAM_ENDPOINT = cfg.jetstream.endpoint;
TANGLED_KNOTSTREAM_RETRY_INTERVAL = cfg.knotstream.retryInterval; TANGLED_KNOTSTREAM_MAX_RETRY_INTERVAL = cfg.knotstream.maxRetryInterval; TANGLED_KNOTSTREAM_CONNECTION_TIMEOUT = cfg.knotstream.connectionTimeout; TANGLED_KNOTSTREAM_WORKER_COUNT = toString cfg.knotstream.workerCount; TANGLED_KNOTSTREAM_QUEUE_SIZE = toString cfg.knotstream.queueSize;
TANGLED_SPINDLESTREAM_RETRY_INTERVAL = cfg.spindlestream.retryInterval; TANGLED_SPINDLESTREAM_MAX_RETRY_INTERVAL = cfg.spindlestream.maxRetryInterval; TANGLED_SPINDLESTREAM_CONNECTION_TIMEOUT = cfg.spindlestream.connectionTimeout; TANGLED_SPINDLESTREAM_WORKER_COUNT = toString cfg.spindlestream.workerCount; TANGLED_SPINDLESTREAM_QUEUE_SIZE = toString cfg.spindlestream.queueSize;
TANGLED_RESEND_SENT_FROM = cfg.resend.sentFrom;
TANGLED_POSTHOG_ENDPOINT = cfg.posthog.endpoint;
TANGLED_CAMO_HOST = cfg.camo.host;
TANGLED_AVATAR_HOST = cfg.avatar.host;
TANGLED_PLC_URL = cfg.plc.url;
TANGLED_PDS_HOST = cfg.pds.host;
TANGLED_LABEL_DEFAULTS = concatStringsSep "," cfg.label.defaults; TANGLED_LABEL_GFI = cfg.label.goodFirstIssue; } // optionalAttrs cfg.ssh.enable { TANGLED_SSH_ENABLED = "true"; TANGLED_SSH_LISTEN_ADDR = cfg.ssh.listenAddr; } // optionalAttrs (cfg.ssh.enable && cfg.ssh.hostKeyPath != null) { TANGLED_SSH_HOST_KEY_PATH = cfg.ssh.hostKeyPath; }; }; }; }