Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

at icy/qmlqxq 14 kB View raw
1{ 2 pkgs, 3 config, 4 lib, 5 ... 6}: let 7 cfg = config.services.tangled.appview; 8in 9 with lib; let 10 effectivePackage = 11 if cfg.project.templatesDir == null 12 then cfg.package 13 else cfg.package.override {customTemplatesDir = cfg.project.templatesDir;}; 14 in { 15 options = { 16 services.tangled.appview = { 17 enable = mkOption { 18 type = types.bool; 19 default = false; 20 description = "Enable tangled appview"; 21 }; 22 23 package = mkOption { 24 type = types.package; 25 description = "Package to use for the appview"; 26 }; 27 28 # core configuration 29 port = mkOption { 30 type = types.port; 31 default = 3000; 32 description = "Port to run the appview on"; 33 }; 34 35 listenAddr = mkOption { 36 type = types.str; 37 default = "0.0.0.0:${toString cfg.port}"; 38 description = "Listen address for the appview service"; 39 }; 40 41 metricsListenAddr = mkOption { 42 type = types.str; 43 default = "0.0.0.0:9090"; 44 description = "Listen address for the Prometheus metrics endpoint"; 45 }; 46 47 dbPath = mkOption { 48 type = types.str; 49 default = "/var/lib/appview/appview.db"; 50 description = "Path to the SQLite database file"; 51 }; 52 53 appviewHost = mkOption { 54 type = types.str; 55 default = "tangled.org"; 56 example = "example.com"; 57 description = "Public host URL for the appview instance"; 58 }; 59 60 appviewName = mkOption { 61 type = types.str; 62 default = "Tangled"; 63 description = "Display name for the appview instance"; 64 }; 65 66 dev = mkOption { 67 type = types.bool; 68 default = false; 69 description = "Enable development mode"; 70 }; 71 72 disallowedNicknamesFile = mkOption { 73 type = types.nullOr types.path; 74 default = null; 75 description = "Path to file containing disallowed nicknames"; 76 }; 77 78 # redis configuration 79 redis = { 80 addr = mkOption { 81 type = types.str; 82 default = "localhost:6379"; 83 description = "Redis server address"; 84 }; 85 86 db = mkOption { 87 type = types.int; 88 default = 0; 89 description = "Redis database number"; 90 }; 91 }; 92 93 # jetstream configuration 94 jetstream = { 95 endpoint = mkOption { 96 type = types.str; 97 default = "wss://jetstream1.us-east.bsky.network/subscribe"; 98 description = "Jetstream WebSocket endpoint"; 99 }; 100 }; 101 102 # knotstream consumer configuration 103 knotstream = { 104 retryInterval = mkOption { 105 type = types.str; 106 default = "60s"; 107 description = "Initial retry interval for knotstream consumer"; 108 }; 109 110 maxRetryInterval = mkOption { 111 type = types.str; 112 default = "120m"; 113 description = "Maximum retry interval for knotstream consumer"; 114 }; 115 116 connectionTimeout = mkOption { 117 type = types.str; 118 default = "5s"; 119 description = "Connection timeout for knotstream consumer"; 120 }; 121 122 workerCount = mkOption { 123 type = types.int; 124 default = 64; 125 description = "Number of workers for knotstream consumer"; 126 }; 127 128 queueSize = mkOption { 129 type = types.int; 130 default = 100; 131 description = "Queue size for knotstream consumer"; 132 }; 133 }; 134 135 # spindlestream consumer configuration 136 spindlestream = { 137 retryInterval = mkOption { 138 type = types.str; 139 default = "60s"; 140 description = "Initial retry interval for spindlestream consumer"; 141 }; 142 143 maxRetryInterval = mkOption { 144 type = types.str; 145 default = "120m"; 146 description = "Maximum retry interval for spindlestream consumer"; 147 }; 148 149 connectionTimeout = mkOption { 150 type = types.str; 151 default = "5s"; 152 description = "Connection timeout for spindlestream consumer"; 153 }; 154 155 workerCount = mkOption { 156 type = types.int; 157 default = 64; 158 description = "Number of workers for spindlestream consumer"; 159 }; 160 161 queueSize = mkOption { 162 type = types.int; 163 default = 100; 164 description = "Queue size for spindlestream consumer"; 165 }; 166 }; 167 168 # resend configuration 169 resend = { 170 sentFrom = mkOption { 171 type = types.str; 172 default = "noreply@notifs.tangled.sh"; 173 description = "Email address to send notifications from"; 174 }; 175 }; 176 177 # posthog configuration 178 posthog = { 179 endpoint = mkOption { 180 type = types.str; 181 default = "https://eu.i.posthog.com"; 182 description = "PostHog API endpoint"; 183 }; 184 }; 185 186 # camo configuration 187 camo = { 188 host = mkOption { 189 type = types.str; 190 default = "https://camo.tangled.sh"; 191 description = "Camo proxy host URL"; 192 }; 193 }; 194 195 # avatar configuration 196 avatar = { 197 host = mkOption { 198 type = types.str; 199 default = "https://avatar.tangled.sh"; 200 description = "Avatar service host URL"; 201 }; 202 }; 203 204 plc = { 205 url = mkOption { 206 type = types.str; 207 default = "https://plc.directory"; 208 description = "PLC directory URL"; 209 }; 210 }; 211 212 pds = { 213 host = mkOption { 214 type = types.str; 215 default = "https://tngl.sh"; 216 description = "PDS host URL"; 217 }; 218 }; 219 220 label = { 221 defaults = mkOption { 222 type = types.listOf types.str; 223 default = [ 224 "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/wontfix" 225 "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue" 226 "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/duplicate" 227 "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/documentation" 228 "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/assignee" 229 ]; 230 description = "Default label definitions"; 231 }; 232 233 goodFirstIssue = mkOption { 234 type = types.str; 235 default = "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue"; 236 description = "Good first issue label definition"; 237 }; 238 }; 239 240 # ssh log server configuration 241 ssh = { 242 enable = mkOption { 243 type = types.bool; 244 default = false; 245 description = "Enable the SSH pipeline log server"; 246 }; 247 248 listenAddr = mkOption { 249 type = types.str; 250 default = "0.0.0.0:3333"; 251 description = "Listen address for the SSH log server"; 252 }; 253 254 hostKeyPath = mkOption { 255 type = types.nullOr types.str; 256 default = null; 257 example = "/var/lib/appview/ssh_host_key"; 258 description = '' 259 Path to the SSH host key file. If null, an ephemeral key is 260 generated on each startup. generate with: 261 262 ssh-keygen -t ed25519 -N "" -f /var/lib/appview/ssh_host_key 263 ''; 264 }; 265 }; 266 267 project = { 268 enable = mkOption { 269 type = types.bool; 270 default = false; 271 description = "Enable project mode: collapses URL namespace to a single user, disables signup and timeline."; 272 }; 273 274 user = mkOption { 275 type = types.str; 276 default = ""; 277 example = "anirudh.fi"; 278 description = "The handle or DID of the project user. Required when project.enable is true."; 279 }; 280 281 templatesDir = mkOption { 282 type = types.nullOr types.path; 283 default = null; 284 example = "./custom-templates"; 285 description = '' 286 Path to a directory of template overrides. Files are copied on 287 top of the default templates at build time, so individual 288 templates can be replaced without forking the whole tree. 289 ''; 290 }; 291 }; 292 293 294 environmentFile = mkOption { 295 type = with types; nullOr path; 296 default = null; 297 example = "/etc/appview.env"; 298 description = '' 299 Additional environment file as defined in {manpage}`systemd.exec(5)`. 300 301 Sensitive secrets such as {env}`TANGLED_COOKIE_SECRET`, 302 {env}`TANGLED_OAUTH_CLIENT_SECRET`, {env}`TANGLED_RESEND_API_KEY`, 303 {env}`TANGLED_CAMO_SHARED_SECRET`, {env}`TANGLED_AVATAR_SHARED_SECRET`, 304 {env}`TANGLED_REDIS_PASS`, {env}`TANGLED_PDS_ADMIN_SECRET`, 305 {env}`TANGLED_KNOT_ADMIN_SECRET`, 306 {env}`TANGLED_CLOUDFLARE_API_TOKEN`, {env}`TANGLED_CLOUDFLARE_ZONE_ID`, 307 {env}`TANGLED_CLOUDFLARE_TURNSTILE_SITE_KEY`, 308 {env}`TANGLED_CLOUDFLARE_TURNSTILE_SECRET_KEY`, 309 {env}`TANGLED_POSTHOG_API_KEY`, {env}`TANGLED_APP_PASSWORD`, 310 and {env}`TANGLED_ALT_APP_PASSWORD` may be passed to the service 311 without making them world readable in the nix store. 312 ''; 313 }; 314 }; 315 }; 316 317 config = mkIf cfg.enable { 318 assertions = [ 319 { 320 assertion = !cfg.project.enable || cfg.project.user != ""; 321 message = "services.tangled.appview.project.user must be set when project.enable is true"; 322 } 323 ]; 324 services.redis.servers.appview = { 325 enable = true; 326 port = 6379; 327 }; 328 329 systemd.services.appview = { 330 description = "tangled appview service"; 331 wantedBy = ["multi-user.target"]; 332 after = ["redis-appview.service" "network-online.target"]; 333 requires = ["redis-appview.service"]; 334 wants = ["network-online.target"]; 335 336 path = [pkgs.diffutils]; 337 338 serviceConfig = { 339 Type = "simple"; 340 ExecStart = "${effectivePackage}/bin/appview"; 341 Restart = "always"; 342 RestartSec = "10s"; 343 EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; 344 345 # state directory 346 StateDirectory = "appview"; 347 WorkingDirectory = "/var/lib/appview"; 348 349 # security hardening 350 NoNewPrivileges = true; 351 PrivateTmp = true; 352 ProtectSystem = "strict"; 353 ProtectHome = true; 354 ReadWritePaths = 355 ["/var/lib/appview"] 356 ++ optionals (cfg.ssh.enable && cfg.ssh.hostKeyPath != null) [cfg.ssh.hostKeyPath]; 357 }; 358 359 environment = 360 { 361 TANGLED_DB_PATH = cfg.dbPath; 362 TANGLED_LISTEN_ADDR = cfg.listenAddr; 363 TANGLED_METRICS_LISTEN_ADDR = cfg.metricsListenAddr; 364 TANGLED_APPVIEW_HOST = cfg.appviewHost; 365 TANGLED_APPVIEW_NAME = cfg.appviewName; 366 TANGLED_DEV = 367 if cfg.dev 368 then "true" 369 else "false"; 370 } 371 // optionalAttrs (cfg.disallowedNicknamesFile != null) { 372 TANGLED_DISALLOWED_NICKNAMES_FILE = cfg.disallowedNicknamesFile; 373 } 374 // { 375 TANGLED_REDIS_ADDR = cfg.redis.addr; 376 TANGLED_REDIS_DB = toString cfg.redis.db; 377 378 TANGLED_JETSTREAM_ENDPOINT = cfg.jetstream.endpoint; 379 380 TANGLED_KNOTSTREAM_RETRY_INTERVAL = cfg.knotstream.retryInterval; 381 TANGLED_KNOTSTREAM_MAX_RETRY_INTERVAL = cfg.knotstream.maxRetryInterval; 382 TANGLED_KNOTSTREAM_CONNECTION_TIMEOUT = cfg.knotstream.connectionTimeout; 383 TANGLED_KNOTSTREAM_WORKER_COUNT = toString cfg.knotstream.workerCount; 384 TANGLED_KNOTSTREAM_QUEUE_SIZE = toString cfg.knotstream.queueSize; 385 386 TANGLED_SPINDLESTREAM_RETRY_INTERVAL = cfg.spindlestream.retryInterval; 387 TANGLED_SPINDLESTREAM_MAX_RETRY_INTERVAL = cfg.spindlestream.maxRetryInterval; 388 TANGLED_SPINDLESTREAM_CONNECTION_TIMEOUT = cfg.spindlestream.connectionTimeout; 389 TANGLED_SPINDLESTREAM_WORKER_COUNT = toString cfg.spindlestream.workerCount; 390 TANGLED_SPINDLESTREAM_QUEUE_SIZE = toString cfg.spindlestream.queueSize; 391 392 TANGLED_RESEND_SENT_FROM = cfg.resend.sentFrom; 393 394 TANGLED_POSTHOG_ENDPOINT = cfg.posthog.endpoint; 395 396 TANGLED_CAMO_HOST = cfg.camo.host; 397 398 TANGLED_AVATAR_HOST = cfg.avatar.host; 399 400 TANGLED_PLC_URL = cfg.plc.url; 401 402 TANGLED_PDS_HOST = cfg.pds.host; 403 404 TANGLED_LABEL_DEFAULTS = concatStringsSep "," cfg.label.defaults; 405 TANGLED_LABEL_GFI = cfg.label.goodFirstIssue; 406 } 407 // optionalAttrs cfg.ssh.enable { 408 TANGLED_SSH_ENABLED = "true"; 409 TANGLED_SSH_LISTEN_ADDR = cfg.ssh.listenAddr; 410 } 411 // optionalAttrs (cfg.ssh.enable && cfg.ssh.hostKeyPath != null) { 412 TANGLED_SSH_HOST_KEY_PATH = cfg.ssh.hostKeyPath; 413 } 414 // optionalAttrs cfg.project.enable { 415 TANGLED_PROJECT_MODE = "true"; 416 TANGLED_PROJECT_USER = cfg.project.user; 417 }; 418 }; 419 }; 420 }