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