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