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