2.58
main.c
1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
3
4#include "xs.h"
5#include "xs_io.h"
6#include "xs_json.h"
7#include "xs_time.h"
8#include "xs_openssl.h"
9
10#include "snac.h"
11
12#include <sys/stat.h>
13
14int usage(void)
15{
16 printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n");
17 printf("Copyright (c) 2022 - 2024 grunfink et al. / MIT license\n");
18 printf("\n");
19 printf("Commands:\n");
20 printf("\n");
21 printf("init [{basedir}] Initializes the data storage\n");
22 printf("upgrade {basedir} Upgrade to a new version\n");
23 printf("adduser {basedir} [{uid}] Adds a new user\n");
24 printf("deluser {basedir} {uid} Deletes a user\n");
25 printf("httpd {basedir} Starts the HTTPD daemon\n");
26 printf("purge {basedir} Purges old data\n");
27 printf("state {basedir} Prints server state\n");
28 printf("webfinger {basedir} {actor} Queries about an actor (@user@host or actor url)\n");
29 printf("queue {basedir} {uid} Processes a user queue\n");
30 printf("follow {basedir} {uid} {actor} Follows an actor\n");
31 printf("unfollow {basedir} {uid} {actor} Unfollows an actor\n");
32 printf("request {basedir} {uid} {url} Requests an object\n");
33 printf("actor {basedir} [{uid}] {url} Requests an actor\n");
34 printf("note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n");
35 printf("boost|announce {basedir} {uid} {url} Boosts (announces) a post\n");
36 printf("unboost {basedir} {uid} {url} Unboosts a post\n");
37 printf("resetpwd {basedir} {uid} Resets the password of a user\n");
38 printf("ping {basedir} {uid} {actor} Pings an actor\n");
39 printf("webfinger_s {basedir} {uid} {actor} Queries about an actor (@user@host or actor url)\n");
40 printf("pin {basedir} {uid} {msg_url} Pins a message\n");
41 printf("unpin {basedir} {uid} {msg_url} Unpins a message\n");
42 printf("bookmark {basedir} {uid} {msg_url} Bookmarks a message\n");
43 printf("unbookmark {basedir} {uid} {msg_url} Unbookmarks a message\n");
44 printf("block {basedir} {instance_url} Blocks a full instance\n");
45 printf("unblock {basedir} {instance_url} Unblocks a full instance\n");
46 printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n");
47 printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n");
48 printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n");
49 printf("search {basedir} {uid} {regex} Searches posts by content\n");
50
51 return 1;
52}
53
54
55char *get_argv(int *argi, int argc, char *argv[])
56{
57 if (*argi < argc)
58 return argv[(*argi)++];
59 else
60 return NULL;
61}
62
63
64#define GET_ARGV() get_argv(&argi, argc, argv)
65
66int main(int argc, char *argv[])
67{
68 char *cmd;
69 char *basedir;
70 char *user;
71 char *url;
72 int argi = 1;
73 snac snac;
74
75 /* ensure group has write access */
76 umask(0007);
77
78 if ((cmd = GET_ARGV()) == NULL)
79 return usage();
80
81 if (strcmp(cmd, "init") == 0) { /** **/
82 /* initialize the data storage */
83 /* ... */
84 basedir = GET_ARGV();
85
86 return snac_init(basedir);
87 }
88
89 if (strcmp(cmd, "upgrade") == 0) { /** **/
90 int ret;
91
92 /* upgrade */
93 if ((basedir = GET_ARGV()) == NULL)
94 return usage();
95
96 if ((ret = srv_open(basedir, 1)) == 1)
97 srv_log(xs_dup("OK"));
98
99 return ret;
100 }
101
102 if (strcmp(cmd, "markdown") == 0) { /** **/
103 /* undocumented, for testing only */
104 xs *c = xs_readall(stdin);
105 xs *fc = not_really_markdown(c, NULL, NULL);
106
107 printf("<html>\n%s\n</html>\n", fc);
108 return 0;
109 }
110
111 if ((basedir = GET_ARGV()) == NULL)
112 return usage();
113
114 if (!srv_open(basedir, 0)) {
115 srv_log(xs_fmt("error opening data storage at %s", basedir));
116 return 1;
117 }
118
119 if (strcmp(cmd, "adduser") == 0) { /** **/
120 user = GET_ARGV();
121
122 return adduser(user);
123
124 return 0;
125 }
126
127 if (strcmp(cmd, "httpd") == 0) { /** **/
128 httpd();
129 srv_free();
130 return 0;
131 }
132
133 if (strcmp(cmd, "purge") == 0) { /** **/
134 purge_all();
135 return 0;
136 }
137
138 if (strcmp(cmd, "state") == 0) { /** **/
139 xs *shm_name = NULL;
140 srv_state *p_state = srv_state_op(&shm_name, 1);
141
142 if (p_state == NULL)
143 return 1;
144
145 srv_state ss = *p_state;
146 int n;
147
148 printf("server: %s (%s)\n", xs_dict_get(srv_config, "host"), USER_AGENT);
149 xs *uptime = xs_str_time_diff(time(NULL) - ss.srv_start_time);
150 printf("uptime: %s\n", uptime);
151 printf("job fifo size (cur): %d\n", ss.job_fifo_size);
152 printf("job fifo size (peak): %d\n", ss.peak_job_fifo_size);
153 char *th_states[] = { "stopped", "waiting", "input", "output" };
154
155 for (n = 0; n < ss.n_threads; n++)
156 printf("thread #%d state: %s\n", n, th_states[ss.th_state[n]]);
157
158 return 0;
159 }
160
161 if ((user = GET_ARGV()) == NULL)
162 return usage();
163
164 if (strcmp(cmd, "block") == 0) { /** **/
165 int ret = instance_block(user);
166
167 if (ret < 0) {
168 fprintf(stderr, "Error blocking instance %s: %d\n", user, ret);
169 return 1;
170 }
171
172 return 0;
173 }
174
175 if (strcmp(cmd, "unblock") == 0) { /** **/
176 int ret = instance_unblock(user);
177
178 if (ret < 0) {
179 fprintf(stderr, "Error unblocking instance %s: %d\n", user, ret);
180 return 1;
181 }
182
183 return 0;
184 }
185
186 if (strcmp(cmd, "webfinger") == 0) { /** **/
187 xs *actor = NULL;
188 xs *uid = NULL;
189 int status;
190
191 status = webfinger_request(user, &actor, &uid);
192
193 printf("status: %d\n", status);
194 if (actor != NULL)
195 printf("actor: %s\n", actor);
196 if (uid != NULL)
197 printf("uid: %s\n", uid);
198
199 return 0;
200 }
201
202 if (argi == argc && strcmp(cmd, "actor") == 0) { /** **/
203 /* query an actor without user (non-signed) */
204 xs *actor = NULL;
205 int status;
206
207 status = actor_request(NULL, user, &actor);
208
209 printf("status: %d\n", status);
210 if (valid_status(status)) {
211 xs_json_dump(actor, 4, stdout);
212 printf("\n");
213 }
214
215 return 0;
216 }
217
218 if (!user_open(&snac, user)) {
219 printf("invalid user '%s'\n", user);
220 return 1;
221 }
222
223 lastlog_write(&snac, "cmdline");
224
225 if (strcmp(cmd, "resetpwd") == 0) { /** **/
226 return resetpwd(&snac);
227 }
228
229 if (strcmp(cmd, "deluser") == 0) { /** **/
230 return deluser(&snac);
231 }
232
233 if (strcmp(cmd, "update") == 0) { /** **/
234 xs *a_msg = msg_actor(&snac);
235 xs *u_msg = msg_update(&snac, a_msg);
236 enqueue_message(&snac, u_msg);
237 return 0;
238 }
239
240 if (strcmp(cmd, "queue") == 0) { /** **/
241 process_user_queue(&snac);
242 return 0;
243 }
244
245 if (strcmp(cmd, "verify_links") == 0) { /** **/
246 verify_links(&snac);
247 return 0;
248 }
249
250 if (strcmp(cmd, "timeline") == 0) { /** **/
251#if 0
252 xs *list = local_list(&snac, XS_ALL);
253 xs *body = html_timeline(&snac, list, 1);
254
255 printf("%s\n", body);
256 user_free(&snac);
257 srv_free();
258#endif
259
260 xs *idx = xs_fmt("%s/private.idx", snac.basedir);
261 xs *list = index_list_desc(idx, 0, 256);
262 xs *tl = timeline_top_level(&snac, list);
263
264 xs_json_dump(tl, 4, stdout);
265
266 return 0;
267 }
268
269 if ((url = GET_ARGV()) == NULL)
270 return usage();
271
272 if (strcmp(cmd, "webfinger_s") == 0) { /** **/
273 xs *actor = NULL;
274 xs *uid = NULL;
275 int status;
276
277 status = webfinger_request_signed(&snac, url, &actor, &uid);
278
279 printf("status: %d\n", status);
280 if (actor != NULL)
281 printf("actor: %s\n", actor);
282 if (uid != NULL)
283 printf("uid: %s\n", uid);
284
285 return 0;
286 }
287
288 if (strcmp(cmd, "boost") == 0 || strcmp(cmd, "announce") == 0) { /** **/
289 xs *msg = msg_admiration(&snac, url, "Announce");
290
291 if (msg != NULL) {
292 enqueue_message(&snac, msg);
293
294 if (dbglevel) {
295 xs_json_dump(msg, 4, stdout);
296 }
297 }
298
299 return 0;
300 }
301
302 if (strcmp(cmd, "unboost") == 0) { /** **/
303 xs *msg = msg_repulsion(&snac, url, "Announce");
304
305 if (msg != NULL) {
306 enqueue_message(&snac, msg);
307
308 if (dbglevel) {
309 xs_json_dump(msg, 4, stdout);
310 }
311 }
312
313 return 0;
314 }
315
316 if (strcmp(cmd, "follow") == 0) { /** **/
317 xs *msg = msg_follow(&snac, url);
318
319 if (msg != NULL) {
320 const char *actor = xs_dict_get(msg, "object");
321
322 following_add(&snac, actor, msg);
323
324 enqueue_output_by_actor(&snac, msg, actor, 0);
325
326 if (dbglevel) {
327 xs_json_dump(msg, 4, stdout);
328 }
329 }
330
331 return 0;
332 }
333
334 if (strcmp(cmd, "unfollow") == 0) { /** **/
335 xs *object = NULL;
336
337 if (valid_status(following_get(&snac, url, &object))) {
338 xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
339
340 following_del(&snac, url);
341
342 enqueue_output_by_actor(&snac, msg, url, 0);
343
344 snac_log(&snac, xs_fmt("unfollowed actor %s", url));
345 }
346 else
347 snac_log(&snac, xs_fmt("actor is not being followed %s", url));
348
349 return 0;
350 }
351
352 if (strcmp(cmd, "limit") == 0) { /** **/
353 int ret;
354
355 if (!following_check(&snac, url))
356 snac_log(&snac, xs_fmt("actor %s is not being followed", url));
357 else
358 if ((ret = limit(&snac, url)) == 0)
359 snac_log(&snac, xs_fmt("actor %s is now limited", url));
360 else
361 snac_log(&snac, xs_fmt("error limiting actor %s (%d)", url, ret));
362
363 return 0;
364 }
365
366 if (strcmp(cmd, "unlimit") == 0) { /** **/
367 int ret;
368
369 if (!following_check(&snac, url))
370 snac_log(&snac, xs_fmt("actor %s is not being followed", url));
371 else
372 if ((ret = unlimit(&snac, url)) == 0)
373 snac_log(&snac, xs_fmt("actor %s is no longer limited", url));
374 else
375 snac_log(&snac, xs_fmt("error unlimiting actor %s (%d)", url, ret));
376
377 return 0;
378 }
379
380 if (strcmp(cmd, "search") == 0) { /** **/
381 int to;
382
383 /* 'url' contains the regex */
384 xs *r = content_search(&snac, url, 1, 0, XS_ALL, 10, &to);
385
386 int c = 0;
387 const char *v;
388
389 /* print results as standalone links */
390 while (xs_list_next(r, &v, &c)) {
391 printf("%s/admin/p/%s\n", snac.actor, v);
392 }
393
394 return 0;
395 }
396
397 if (strcmp(cmd, "ping") == 0) { /** **/
398 xs *actor_o = NULL;
399
400 if (!xs_startswith(url, "https:/")) {
401 /* try to resolve via webfinger */
402 if (!valid_status(webfinger_request(url, &url, NULL))) {
403 srv_log(xs_fmt("cannot resolve %s via webfinger", url));
404 return 1;
405 }
406 }
407
408 if (valid_status(actor_request(&snac, url, &actor_o))) {
409 xs *msg = msg_ping(&snac, url);
410
411 enqueue_output_by_actor(&snac, msg, url, 0);
412
413 if (dbglevel) {
414 xs_json_dump(msg, 4, stdout);
415 }
416
417 srv_log(xs_fmt("Ping sent to %s -- see log for Pong reply", url));
418 }
419 else {
420 srv_log(xs_fmt("Error getting actor %s", url));
421 return 1;
422 }
423
424 return 0;
425 }
426
427 if (strcmp(cmd, "pin") == 0) { /** **/
428 int ret = pin(&snac, url);
429 if (ret < 0) {
430 fprintf(stderr, "error pinning %s %d\n", url, ret);
431 return 1;
432 }
433
434 return 0;
435 }
436
437 if (strcmp(cmd, "unpin") == 0) { /** **/
438 int ret = unpin(&snac, url);
439 if (ret < 0) {
440 fprintf(stderr, "error unpinning %s %d\n", url, ret);
441 return 1;
442 }
443
444 return 0;
445 }
446
447 if (strcmp(cmd, "bookmark") == 0) { /** **/
448 int ret = bookmark(&snac, url);
449 if (ret < 0) {
450 fprintf(stderr, "error bookmarking %s %d\n", url, ret);
451 return 1;
452 }
453
454 return 0;
455 }
456
457 if (strcmp(cmd, "unbookmark") == 0) { /** **/
458 int ret = unbookmark(&snac, url);
459 if (ret < 0) {
460 fprintf(stderr, "error unbookmarking %s %d\n", url, ret);
461 return 1;
462 }
463
464 return 0;
465 }
466
467 if (strcmp(cmd, "question") == 0) { /** **/
468 int end_secs = 5 * 60;
469 xs *opts = xs_split(url, ";");
470
471 xs *msg = msg_question(&snac, "Poll", NULL, opts, 0, end_secs);
472 xs *c_msg = msg_create(&snac, msg);
473
474 if (dbglevel) {
475 xs_json_dump(c_msg, 4, stdout);
476 }
477
478 enqueue_message(&snac, c_msg);
479 enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
480
481 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
482
483 return 0;
484 }
485
486 if (strcmp(cmd, "request") == 0) { /** **/
487 int status;
488 xs *data = NULL;
489
490 status = activitypub_request(&snac, url, &data);
491
492 printf("status: %d\n", status);
493
494 if (data != NULL) {
495 xs_json_dump(data, 4, stdout);
496 }
497
498 return 0;
499 }
500
501 if (strcmp(cmd, "request2") == 0) { /** **/
502 enqueue_object_request(&snac, url, 2);
503
504 return 0;
505 }
506
507 if (strcmp(cmd, "actor") == 0) { /** **/
508 int status;
509 xs *data = NULL;
510
511 status = actor_request(&snac, url, &data);
512
513 printf("status: %d\n", status);
514
515 if (valid_status(status)) {
516 xs_json_dump(data, 4, stdout);
517 }
518
519 return 0;
520 }
521
522 if (strcmp(cmd, "note") == 0) { /** **/
523 xs *content = NULL;
524 xs *msg = NULL;
525 xs *c_msg = NULL;
526 xs *attl = xs_list_new();
527 char *fn = NULL;
528
529 /* iterate possible attachments */
530 while ((fn = GET_ARGV())) {
531 FILE *f;
532
533 if ((f = fopen(fn, "rb")) != NULL) {
534 /* get the file size and content */
535 fseek(f, 0, SEEK_END);
536 int sz = ftell(f);
537 fseek(f, 0, SEEK_SET);
538 xs *atc = xs_readall(f);
539 fclose(f);
540
541 char *ext = strrchr(fn, '.');
542 xs *hash = xs_md5_hex(fn, strlen(fn));
543 xs *id = xs_fmt("%s%s", hash, ext);
544 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
545
546 /* store */
547 static_put(&snac, id, atc, sz);
548
549 xs *l = xs_list_new();
550
551 l = xs_list_append(l, url);
552 l = xs_list_append(l, ""); /* alt text */
553
554 attl = xs_list_append(attl, l);
555 }
556 else
557 fprintf(stderr, "Error opening '%s' as attachment\n", fn);
558 }
559
560 if (strcmp(url, "-e") == 0) {
561 /* get the content from an editor */
562 FILE *f;
563
564 unlink("/tmp/snac-edit.txt");
565 system("$EDITOR /tmp/snac-edit.txt");
566
567 if ((f = fopen("/tmp/snac-edit.txt", "r")) != NULL) {
568 content = xs_readall(f);
569 fclose(f);
570
571 unlink("/tmp/snac-edit.txt");
572 }
573 else {
574 printf("Nothing to send\n");
575 return 1;
576 }
577 }
578 else
579 if (strcmp(url, "-") == 0) {
580 /* get the content from stdin */
581 content = xs_readall(stdin);
582 }
583 else
584 content = xs_dup(url);
585
586 msg = msg_note(&snac, content, NULL, NULL, attl, 0);
587
588 c_msg = msg_create(&snac, msg);
589
590 if (dbglevel) {
591 xs_json_dump(c_msg, 4, stdout);
592 }
593
594 enqueue_message(&snac, c_msg);
595
596 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
597
598 return 0;
599 }
600
601 fprintf(stderr, "ERROR: bad command '%s'\n", cmd);
602
603 return 1;
604}