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