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