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