Monorepo for Tangled tangled.org
5

Configure Feed

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

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