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