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