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