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