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 }