2.88 main.c
  1/* snac - A simple, minimalistic ActivityPub instance */
  2/* copyright (c) 2022 - 2026 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#include "xs_random.h"
 11#include "xs_http.h"
 12
 13#include "snac.h"
 14
 15#include <sys/stat.h>
 16#include <sys/wait.h>
 17
 18int usage(const char *cmd)
 19{
 20    printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n");
 21    printf("Copyright (c) 2022 - 2026 grunfink et al. / MIT license\n");
 22    printf("\n");
 23
 24    if (cmd == NULL) {
 25        printf("Commands:\n");
 26        printf("\n");
 27    }
 28
 29    const char *cmds =
 30        "init [{basedir}]                     Initializes the data storage\n"
 31        "upgrade {basedir}                    Upgrade to a new version\n"
 32        "adduser {basedir} [{uid}]            Adds a new user\n"
 33        "deluser {basedir} {uid}              Deletes a user\n"
 34        "update {basedir} {uid}               Sends a user's updated profile\n"
 35        "httpd {basedir}                      Starts the HTTPD daemon\n"
 36        "purge {basedir}                      Purges old data\n"
 37        "state {basedir}                      Prints server state\n"
 38        "webfinger {basedir} {account}        Queries about an account (@user@host or actor url)\n"
 39        "queue {basedir} {uid}                Processes a user queue\n"
 40        "follow {basedir} {uid} {actor}       Follows an actor\n"
 41        "unfollow {basedir} {uid} {actor}     Unfollows an actor\n"
 42        "request {basedir} {uid} {url}        Requests an object\n"
 43        "insert {basedir} {uid} {url}         Requests an object and inserts it into the timeline\n"
 44        "collect_replies {basedir} {uid} {url} Collects all replies from a post\n"
 45        "actor {basedir} [{uid}] {url}        Requests an actor\n"
 46        "note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n"
 47        "note_unlisted {basedir} {uid} {text} [files...] Sends an unlisted note with optional attachments\n"
 48        "note_mention {basedir} {uid} {text} [files...] Sends a note only to mentioned accounts\n"
 49        "note_followers {basedir} {uid} {text} [files...] Sends a note only to followers\n"
 50        "boost|announce {basedir} {uid} {url} Boosts (announces) a post\n"
 51        "unboost {basedir} {uid} {url}        Unboosts a post\n"
 52        "resetpwd {basedir} {uid}             Resets the password of a user\n"
 53        "ping {basedir} {uid} {actor}         Pings an actor\n"
 54        "webfinger_s {basedir} {uid} {account} Queries about an account (@user@host or actor url)\n"
 55        "pin {basedir} {uid} {msg_url}        Pins a message\n"
 56        "unpin {basedir} {uid} {msg_url}      Unpins a message\n"
 57        "bookmark {basedir} {uid} {msg_url}   Bookmarks a message\n"
 58        "unbookmark {basedir} {uid} {msg_url} Unbookmarks a message\n"
 59        "block {basedir} {instance_url}       Blocks a full instance\n"
 60        "unblock {basedir} {instance_url}     Unblocks a full instance\n"
 61        "limit {basedir} {uid} {actor}        Limits an actor (drops their announces)\n"
 62        "unlimit {basedir} {uid} {actor}      Unlimits an actor\n"
 63        "muted {basedir} {uid}                Lists the muted actors\n"
 64        "unmute {basedir} {uid} {actor}       Unmutes a previously muted actor\n"
 65        "verify_links {basedir} {uid}         Verifies a user's links (in the metadata)\n"
 66        "search {basedir} {uid} {regex}       Searches posts by content\n"
 67        "export_csv {basedir} {uid}           Exports followers, lists, MUTEd and bookmarks to CSV\n"
 68        "export_posts {basedir} {uid}         Exports all posts to outbox.json\n"
 69        "alias {basedir} {uid} {account}      Sets account (@user@host or actor url) as an alias\n"
 70        "migrate {basedir} {uid}              Migrates to the account defined as the alias\n"
 71        "import_csv {basedir} {uid}           Imports data from CSV files\n"
 72        "import_list {basedir} {uid} {file}   Imports a Mastodon CSV list file\n"
 73        "import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n"
 74        "lists {basedir} {uid}                Returns the names of the lists created by the user\n"
 75        "list_members {basedir} {uid} {name}  Returns the list of accounts inside a list\n"
 76        "list_create {basedir} {uid} {name}   Creates a new list\n"
 77        "list_remove {basedir} {uid} {name}   Removes an existing list\n"
 78        "list_add {basedir} {uid} {name} {acct} Adds an account (@user@host or actor url) to a list\n"
 79        "list_del {basedir} {uid} {name} {actor} Deletes an actor URL from a list\n"
 80        "top_ten {basedir} {uid} [{N}]        Prints the most popular posts\n";
 81
 82    if (cmd == NULL)
 83        printf("%s", cmds);
 84    else {
 85        /* only show help for the entered command */
 86        xs *l = xs_split(cmds, "\n");
 87        const char *v;
 88        int cnt = 0;
 89
 90        xs_list_foreach(l, v) {
 91            if (xs_str_in(v, cmd) != -1) {
 92                printf("%s\n", v);
 93                cnt++;
 94            }
 95        }
 96
 97        if (cnt == 0)
 98            printf("%s", cmds);
 99    }
100
101    return 1;
102}
103
104
105char *get_argv(int *argi, int argc, char *argv[])
106{
107    if (*argi < argc)
108        return argv[(*argi)++];
109    else
110        return NULL;
111}
112
113
114#define GET_ARGV() get_argv(&argi, argc, argv)
115
116int main(int argc, char *argv[])
117{
118    char *cmd;
119    char *basedir;
120    char *user;
121    char *url;
122    int argi = 1;
123    snac snac;
124
125    /* ensure group has write access */
126    umask(0007);
127
128    if ((cmd = GET_ARGV()) == NULL)
129        return usage(cmd);
130
131    if (strcmp(cmd, "init") == 0) { /** **/
132        /* initialize the data storage */
133        /* ... */
134        basedir = GET_ARGV();
135
136        return snac_init(basedir);
137    }
138
139    if ((basedir = getenv("SNAC_BASEDIR")) == NULL) {
140        if ((basedir = GET_ARGV()) == NULL)
141            return usage(cmd);
142    }
143
144    if (strcmp(cmd, "upgrade") == 0) { /** **/
145        int ret;
146
147        /* upgrade */
148        if ((ret = srv_open(basedir, 1)) == 1)
149            srv_log(xs_dup("OK"));
150
151        return ret;
152    }
153
154    if (!srv_open(basedir, 0)) {
155        srv_log(xs_fmt("error opening data storage at %s", basedir));
156        return 1;
157    }
158
159    if (strcmp(cmd, "adduser") == 0) { /** **/
160        user = GET_ARGV();
161
162        return adduser(user);
163
164        return 0;
165    }
166
167    if (strcmp(cmd, "httpd") == 0) { /** **/
168        httpd();
169        srv_free();
170        return 0;
171    }
172
173    if (strcmp(cmd, "purge") == 0) { /** **/
174        purge_all();
175        return 0;
176    }
177
178    if (strcmp(cmd, "poll_hashtag_rss") == 0) { /** **/
179        rss_poll_hashtags();
180        return 0;
181    }
182
183    if (strcmp(cmd, "state") == 0) { /** **/
184        xs *shm_name = NULL;
185        srv_state *p_state = srv_state_op(&shm_name, 1);
186
187        if (p_state == NULL)
188            return 1;
189
190        srv_state ss = *p_state;
191        int n;
192
193        printf("server: %s (%s)\n", xs_dict_get(srv_config, "host"), USER_AGENT);
194        xs *uptime = xs_str_time_diff(time(NULL) - ss.srv_start_time);
195        printf("uptime: %s\n", uptime);
196        printf("job fifo size (cur): %d\n", ss.job_fifo_size);
197        printf("job fifo size (peak): %d\n", ss.peak_job_fifo_size);
198        char *th_states[] = { "stopped", "waiting", "input", "output" };
199
200        for (n = 0; n < ss.n_threads; n++)
201            printf("thread #%d state: %s\n", n, th_states[ss.th_state[n]]);
202
203        return 0;
204    }
205
206    if ((user = GET_ARGV()) == NULL)
207        return usage(cmd);
208
209    if (strcmp(cmd, "block") == 0) { /** **/
210        int ret = instance_block(user);
211
212        if (ret < 0) {
213            fprintf(stderr, "Error blocking instance %s: %d\n", user, ret);
214            return 1;
215        }
216
217        return 0;
218    }
219
220    if (strcmp(cmd, "unblock") == 0) { /** **/
221        int ret = instance_unblock(user);
222
223        if (ret < 0) {
224            fprintf(stderr, "Error unblocking instance %s: %d\n", user, ret);
225            return 1;
226        }
227
228        return 0;
229    }
230
231    if (strcmp(cmd, "webfinger") == 0) { /** **/
232        xs *actor = NULL;
233        xs *uid = NULL;
234        int status;
235
236        status = webfinger_request(user, &actor, &uid);
237
238        printf("status: %d\n", status);
239        if (actor != NULL)
240            printf("actor: %s\n", actor);
241        if (uid != NULL)
242            printf("uid: %s\n", uid);
243
244        return 0;
245    }
246
247    if (argi == argc && strcmp(cmd, "actor") == 0) { /** **/
248        /* query an actor without user (non-signed) */
249        xs *actor = NULL;
250        int status;
251
252        status = actor_request(NULL, user, &actor);
253
254        printf("status: %d\n", status);
255        if (valid_status(status)) {
256            xs_json_dump(actor, 4, stdout);
257            printf("\n");
258        }
259
260        return 0;
261    }
262
263    if (!user_open(&snac, user)) {
264        printf("invalid user '%s'\n", user);
265        return 1;
266    }
267
268    lastlog_write(&snac, "cmdline");
269
270    if (strcmp(cmd, "resetpwd") == 0) { /** **/
271        return resetpwd(&snac);
272    }
273
274    if (strcmp(cmd, "deluser") == 0) { /** **/
275        return deluser(&snac);
276    }
277
278    if (strcmp(cmd, "update") == 0) { /** **/
279        xs *a_msg = msg_actor(&snac);
280        xs *u_msg = msg_update(&snac, a_msg);
281        enqueue_message(&snac, u_msg);
282        return 0;
283    }
284
285    if (strcmp(cmd, "queue") == 0) { /** **/
286        process_user_queue(&snac);
287        return 0;
288    }
289
290    if (strcmp(cmd, "verify_links") == 0) { /** **/
291        verify_links(&snac);
292        return 0;
293    }
294
295    if (strcmp(cmd, "timeline") == 0) { /** **/
296#if 0
297        xs *list = local_list(&snac, XS_ALL);
298        xs *body = html_timeline(&snac, list, 1);
299
300        printf("%s\n", body);
301        user_free(&snac);
302        srv_free();
303#endif
304
305        xs *idx  = xs_fmt("%s/private.idx", snac.basedir);
306        xs *list = index_list_desc(idx, 0, 256);
307        xs *tl   = timeline_top_level(&snac, list);
308
309        xs_json_dump(tl, 4, stdout);
310
311        return 0;
312    }
313
314    if (strcmp(cmd, "export_csv") == 0) { /** **/
315        export_csv(&snac);
316        return 0;
317    }
318
319    if (strcmp(cmd, "export_posts") == 0) { /** **/
320        export_posts(&snac);
321        return 0;
322    }
323
324    if (strcmp(cmd, "import_csv") == 0) { /** **/
325        import_csv(&snac);
326        return 0;
327    }
328
329    if (strcmp(cmd, "migrate") == 0) { /** **/
330        return migrate_account(&snac);
331    }
332
333    if (strcmp(cmd, "lists") == 0) { /** **/
334        xs *lol = list_maint(&snac, NULL, 0);
335        const xs_list *l;
336
337        xs_list_foreach(lol, l) {
338            printf("%s (%s)\n", xs_list_get(l, 1), xs_list_get(l, 0));
339        }
340
341        return 0;
342    }
343
344    if (strcmp(cmd, "muted") == 0) { /** **/
345        xs *l = muted_list(&snac);
346        const char *v;
347
348        xs_list_foreach(l, v)
349            printf("%s\n", v);
350
351        return 0;
352    }
353
354    if (strcmp(cmd, "top_ten") == 0) { /** **/
355        int count = 10;
356        const char *n = GET_ARGV();
357        if (xs_is_string(n))
358            count = atoi(n);
359
360        xs *l = user_top_ten(&snac, count);
361        const xs_list *i;
362
363        xs_list_foreach(l, i) {
364            printf("%s %ld★ %ld↺\n", xs_list_get(i, 0),
365                xs_number_get_l(xs_list_get(i, 1)),
366                xs_number_get_l(xs_list_get(i, 2)));
367        }
368
369        return 0;
370    }
371
372    if ((url = GET_ARGV()) == NULL)
373        return usage(cmd);
374
375    if (strcmp(cmd, "list_members") == 0) { /** **/
376        xs *lid = list_maint(&snac, url, 4);
377
378        if (lid != NULL) {
379            xs *lcont = list_members(&snac, lid, NULL, 0);
380            const char *md5;
381
382            xs_list_foreach(lcont, md5) {
383                xs *actor = NULL;
384
385                if (valid_status(object_get_by_md5(md5, &actor))) {
386                    printf("%s (%s)\n", xs_dict_get(actor, "id"), xs_dict_get_def(actor, "preferredUsername", ""));
387                }
388            }
389        }
390        else
391            fprintf(stderr, "Cannot find a list named '%s'\n", url);
392
393        return 0;
394    }
395
396    if (strcmp(cmd, "list_create") == 0) { /** **/
397        xs *lid = list_maint(&snac, url, 4);
398
399        if (lid == NULL) {
400            xs *n_lid = list_maint(&snac, url, 1);
401            printf("New list named '%s' created (%s)\n", url, n_lid);
402        }
403        else
404            fprintf(stderr, "A list named '%s' already exist\n", url);
405
406        return 0;
407    }
408
409    if (strcmp(cmd, "list_remove") == 0) { /** **/
410        xs *lid = list_maint(&snac, url, 4);
411
412        if (lid != NULL) {
413            list_maint(&snac, lid, 2);
414            printf("List '%s' (%s) deleted\n", url, lid);
415        }
416        else
417            fprintf(stderr, "Cannot find a list named '%s'\n", url);
418
419        return 0;
420    }
421
422    if (strcmp(cmd, "list_add") == 0) { /** **/
423        const char *account = GET_ARGV();
424
425        if (account != NULL) {
426            xs *lid = list_maint(&snac, url, 4);
427
428            if (lid != NULL) {
429                xs *actor = NULL;
430                xs *uid = NULL;
431
432                if (valid_status(webfinger_request(account, &actor, &uid))) {
433                    xs *md5 = xs_md5_hex(actor, strlen(actor));
434
435                    list_members(&snac, lid, md5, 1);
436                    printf("Actor %s (%s) added to list '%s' (%s)\n", actor, uid, url, lid);
437                }
438                else
439                    fprintf(stderr, "Cannot resolve account '%s'\n", account);
440            }
441            else
442                fprintf(stderr, "Cannot find a list named '%s'\n", url);
443
444        }
445
446        return 0;
447    }
448
449    if (strcmp(cmd, "list_del") == 0) { /** **/
450        const char *account = GET_ARGV();
451
452        if (account != NULL) {
453            xs *lid = list_maint(&snac, url, 4);
454
455            if (lid != NULL) {
456                xs *md5 = xs_md5_hex(account, strlen(account));
457
458                list_members(&snac, lid, md5, 2);
459                printf("Actor %s deleted from list '%s' (%s)\n", account, url, lid);
460            }
461            else
462                fprintf(stderr, "Cannot find a list named '%s'\n", url);
463
464        }
465
466        return 0;
467    }
468
469    if (strcmp(cmd, "alias") == 0) { /** **/
470        xs *actor = NULL;
471        xs *uid = NULL;
472        int status = HTTP_STATUS_OK;
473
474        if (*url == '\0')
475            actor = xs_dup("");
476        else
477            status = webfinger_request(url, &actor, &uid);
478
479        if (valid_status(status)) {
480            if (strcmp(actor, snac.actor) == 0) {
481                snac_log(&snac, xs_fmt("You can't be your own alias"));
482                return 1;
483            }
484            else {
485                snac.config = xs_dict_set(snac.config, "alias", actor);
486                snac.config = xs_dict_set(snac.config, "alias_raw", url);
487
488                user_persist(&snac, 1);
489            }
490        }
491        else {
492            snac_log(&snac, xs_fmt("Webfinger error for %s %d", url, status));
493            return 1;
494        }
495
496        return 0;
497    }
498
499    if (strcmp(cmd, "webfinger_s") == 0) { /** **/
500        xs *actor = NULL;
501        xs *uid = NULL;
502        int status;
503
504        status = webfinger_request_signed(&snac, url, &actor, &uid);
505
506        printf("status: %d\n", status);
507        if (actor != NULL)
508            printf("actor: %s\n", actor);
509        if (uid != NULL)
510            printf("uid: %s\n", uid);
511
512        return 0;
513    }
514
515    if (strcmp(cmd, "boost") == 0 || strcmp(cmd, "announce") == 0) { /** **/
516        xs *msg = msg_admiration(&snac, url, "Announce");
517
518        if (msg != NULL) {
519            enqueue_message(&snac, msg);
520            timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, "");
521
522            if (dbglevel) {
523                xs_json_dump(msg, 4, stdout);
524            }
525        }
526
527        return 0;
528    }
529
530
531    if (strcmp(cmd, "assist") == 0) { /** **/
532        /* undocumented: experimental (do not use) */
533        xs *msg = msg_admiration(&snac, url, "Accept");
534
535        if (msg != NULL) {
536            enqueue_message(&snac, msg);
537
538            if (dbglevel) {
539                xs_json_dump(msg, 4, stdout);
540            }
541        }
542
543        return 0;
544    }
545
546    if (strcmp(cmd, "unboost") == 0) { /** **/
547        xs *msg = msg_repulsion(&snac, url, "Announce");
548
549        if (msg != NULL) {
550            enqueue_message(&snac, msg);
551
552            if (dbglevel) {
553                xs_json_dump(msg, 4, stdout);
554            }
555        }
556
557        return 0;
558    }
559
560    if (strcmp(cmd, "follow") == 0) { /** **/
561        xs *msg = msg_follow(&snac, url);
562
563        if (msg != NULL) {
564            const char *actor = xs_dict_get(msg, "object");
565
566            following_add(&snac, actor, msg);
567
568            enqueue_output_by_actor(&snac, msg, actor, 0);
569
570            if (dbglevel) {
571                xs_json_dump(msg, 4, stdout);
572            }
573        }
574
575        return 0;
576    }
577
578    if (strcmp(cmd, "unfollow") == 0) { /** **/
579        xs *object = NULL;
580
581        if (valid_status(following_get(&snac, url, &object))) {
582            xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
583
584            following_del(&snac, url);
585
586            enqueue_output_by_actor(&snac, msg, url, 0);
587
588            snac_log(&snac, xs_fmt("unfollowed actor %s", url));
589        }
590        else
591            snac_log(&snac, xs_fmt("actor is not being followed %s", url));
592
593        return 0;
594    }
595
596    if (strcmp(cmd, "limit") == 0) { /** **/
597        int ret;
598
599        if (!following_check(&snac, url))
600            snac_log(&snac, xs_fmt("actor %s is not being followed", url));
601        else
602        if ((ret = limit(&snac, url)) == 0)
603            snac_log(&snac, xs_fmt("actor %s is now limited", url));
604        else
605            snac_log(&snac, xs_fmt("error limiting actor %s (%d)", url, ret));
606
607        return 0;
608    }
609
610    if (strcmp(cmd, "unlimit") == 0) { /** **/
611        int ret;
612
613        if (!following_check(&snac, url))
614            snac_log(&snac, xs_fmt("actor %s is not being followed", url));
615        else
616        if ((ret = unlimit(&snac, url)) == 0)
617            snac_log(&snac, xs_fmt("actor %s is no longer limited", url));
618        else
619            snac_log(&snac, xs_fmt("error unlimiting actor %s (%d)", url, ret));
620
621        return 0;
622    }
623
624    if (strcmp(cmd, "unmute") == 0) { /** **/
625        if (is_muted(&snac, url)) {
626            unmute(&snac, url);
627
628            printf("%s unmuted\n", url);
629        }
630        else
631            printf("%s actor is not muted\n", url);
632
633        return 0;
634    }
635
636    if (strcmp(cmd, "search") == 0) { /** **/
637        int to;
638
639        /* 'url' contains the regex */
640        xs *r = content_search(&snac, url, 1, 0, XS_ALL, 10, &to);
641
642        int c = 0;
643        const char *v;
644
645        /* print results as standalone links */
646        while (xs_list_next(r, &v, &c)) {
647            printf("%s/admin/p/%s\n", snac.actor, v);
648        }
649
650        return 0;
651    }
652
653    if (strcmp(cmd, "ping") == 0) { /** **/
654        xs *actor_o = NULL;
655
656        if (!xs_startswith(url, "https:/")) {
657            /* try to resolve via webfinger */
658            if (!valid_status(webfinger_request(url, &url, NULL))) {
659                srv_log(xs_fmt("cannot resolve %s via webfinger", url));
660                return 1;
661            }
662        }
663
664        if (valid_status(actor_request(&snac, url, &actor_o))) {
665            xs *msg = msg_ping(&snac, url);
666
667            enqueue_output_by_actor(&snac, msg, url, 0);
668
669            if (dbglevel) {
670                xs_json_dump(msg, 4, stdout);
671            }
672
673            srv_log(xs_fmt("Ping sent to %s -- see log for Pong reply", url));
674        }
675        else {
676            srv_log(xs_fmt("Error getting actor %s", url));
677            return 1;
678        }
679
680        return 0;
681    }
682
683    if (strcmp(cmd, "pin") == 0) { /** **/
684        int ret = pin(&snac, url);
685        if (ret < 0) {
686            fprintf(stderr, "error pinning %s %d\n", url, ret);
687            return 1;
688        }
689
690        return 0;
691    }
692
693    if (strcmp(cmd, "unpin") == 0) { /** **/
694        int ret = unpin(&snac, url);
695        if (ret < 0) {
696            fprintf(stderr, "error unpinning %s %d\n", url, ret);
697            return 1;
698        }
699
700        return 0;
701    }
702
703    if (strcmp(cmd, "bookmark") == 0) { /** **/
704        int ret = bookmark(&snac, url);
705        if (ret < 0) {
706            fprintf(stderr, "error bookmarking %s %d\n", url, ret);
707            return 1;
708        }
709
710        return 0;
711    }
712
713    if (strcmp(cmd, "unbookmark") == 0) { /** **/
714        int ret = unbookmark(&snac, url);
715        if (ret < 0) {
716            fprintf(stderr, "error unbookmarking %s %d\n", url, ret);
717            return 1;
718        }
719
720        return 0;
721    }
722
723    if (strcmp(cmd, "question") == 0) { /** **/
724        int end_secs = 5 * 60;
725        xs *opts = xs_split(url, ";");
726
727        xs *msg = msg_question(&snac, "Poll", NULL, opts, 0, end_secs);
728        xs *c_msg = msg_create(&snac, msg);
729
730        if (dbglevel) {
731            xs_json_dump(c_msg, 4, stdout);
732        }
733
734        enqueue_message(&snac, c_msg);
735        enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
736
737        timeline_add(&snac, xs_dict_get(msg, "id"), msg);
738
739        return 0;
740    }
741
742    if (strcmp(cmd, "request") == 0) { /** **/
743        int status;
744        xs *data = NULL;
745
746        status = activitypub_request(&snac, url, &data);
747
748        printf("status: %d\n", status);
749
750        if (data != NULL) {
751            xs_json_dump(data, 4, stdout);
752        }
753
754        return 0;
755    }
756
757    if (strcmp(cmd, "collect_replies") == 0) { /** **/
758        enqueue_collect_replies(&snac, url);
759
760        return 0;
761    }
762
763    if (strcmp(cmd, "collect_outbox") == 0) { /** **/
764        enqueue_collect_outbox(&snac, url);
765        return 0;
766    }
767
768    if (strcmp(cmd, "insert") == 0) { /** **/
769        int status;
770        xs *data = NULL;
771
772        status = activitypub_request(&snac, url, &data);
773
774        printf("status: %d\n", status);
775
776        if (data != NULL) {
777            xs_json_dump(data, 4, stdout);
778            enqueue_actor_refresh(&snac, xs_dict_get(data, "attributedTo"), 0);
779
780            if (!timeline_here(&snac, url))
781                timeline_add(&snac, url, data);
782            else
783                printf("Post %s already here\n", url);
784        }
785
786        return 0;
787    }
788
789    if (strcmp(cmd, "request2") == 0) { /** **/
790        enqueue_object_request(&snac, url, 2);
791
792        return 0;
793    }
794
795    if (strcmp(cmd, "actor") == 0) { /** **/
796        int status;
797        xs *data = NULL;
798
799        status = actor_request(&snac, url, &data);
800
801        printf("status: %d\n", status);
802
803        if (valid_status(status)) {
804            xs_json_dump(data, 4, stdout);
805        }
806
807        return 0;
808    }
809
810    if (strcmp(cmd, "import_list") == 0) { /** **/
811        import_list_csv(&snac, url);
812
813        return 0;
814    }
815
816    if (strcmp(cmd, "import_block_list") == 0) { /** **/
817        import_blocked_accounts_csv(&snac, url);
818
819        return 0;
820    }
821
822    if (strcmp(cmd, "note") == 0 ||             /** **/
823        strcmp(cmd, "note_unlisted") == 0 ||    /** **/
824        strcmp(cmd, "note_mention") == 0 ||     /** **/
825        strcmp(cmd, "note_followers") == 0) {   /** **/
826        xs *content = NULL;
827        xs *msg = NULL;
828        xs *c_msg = NULL;
829        xs *attl = xs_list_new();
830        const char *fn = NULL;
831        const char *in_reply_to = NULL;
832        const char **arg_irt = NULL;
833        int arg_date = 0;
834        xs *post_date = NULL;
835
836        /* iterate possible attachments */
837        while ((fn = GET_ARGV())) {
838            FILE *f;
839
840            if (arg_irt) {
841                *arg_irt = fn;
842                arg_irt = NULL;
843            }
844            else
845            if (arg_date) {
846                /* convert to ISO */
847                time_t t = xs_parse_localtime(fn, "%Y%m%d%H%M%S");
848
849                if (t == 0) {
850                    fprintf(stderr, "Invalid scheduled date format (must be YYYYmmddHHMMSS)\n");
851                    return 1;
852                }
853
854                post_date = xs_str_iso_date(t);
855                arg_date = 0;
856            }
857            else
858            if (strcmp(fn, "-r") == 0) {
859                /* next argument is an inReplyTo */
860                arg_irt = &in_reply_to;
861            }
862            else
863            if (strcmp(fn, "-d") == 0) {
864                /* next argument is the schedule date */
865                arg_date = 1;
866            }
867            else
868            if ((f = fopen(fn, "rb")) != NULL) {
869                /* get the file size and content */
870                fseek(f, 0, SEEK_END);
871                int sz = ftell(f);
872                fseek(f, 0, SEEK_SET);
873                xs *atc = xs_readall(f);
874                fclose(f);
875
876                char *ext = strrchr(fn, '.');
877                char rnd[32];
878                xs_rnd_buf(rnd, sizeof(rnd));
879                xs *hash  = xs_md5_hex(rnd, sizeof(rnd));
880                xs *id    = xs_fmt("post-%s%s", hash, ext ? ext : "");
881                xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
882
883                /* store */
884                static_put(&snac, id, atc, sz);
885
886                xs *l = xs_list_new();
887
888                l = xs_list_append(l, url);
889                l = xs_list_append(l, ""); /* alt text */
890
891                attl = xs_list_append(attl, l);
892            }
893            else
894                fprintf(stderr, "Error opening '%s' as attachment\n", fn);
895        }
896
897        if (strcmp(url, "-e") == 0) {
898            /* get the content from an editor */
899#define EDITOR "$EDITOR "
900            char cmd[] = EDITOR "/tmp/snac-XXXXXX";
901            FILE *f;
902            int fd = mkstemp(cmd + strlen(EDITOR));
903
904            if (fd >= 0) {
905                int status = system(cmd);
906
907                if (WIFEXITED(status) && WEXITSTATUS(status) == 0 && (f = fdopen(fd, "r")) != NULL) {
908                    content = xs_readall(f);
909                    fclose(f);
910                    unlink(cmd + strlen(EDITOR));
911                } else {
912                    printf("Nothing to send\n");
913                    close(fd);
914                    return 1;
915                }
916            } else {
917                fprintf(stderr, "Temp file creation failed\n");
918                return 1;
919            }
920        }
921        else
922        if (strcmp(url, "-") == 0) {
923            /* get the content from stdin */
924            content = xs_readall(stdin);
925        }
926        else
927            content = xs_dup(url);
928
929        if (!content || !*content) {
930            fprintf(stderr, "Nothing to send\n");
931            return 1;
932        }
933
934        int scope = SCOPE_PUBLIC;
935        if (strcmp(cmd, "note_mention") == 0)
936            scope = SCOPE_MENTIONED;
937        else
938        if (strcmp(cmd, "note_unlisted") == 0)
939            scope = SCOPE_UNLISTED;
940        else
941        if (strcmp(cmd, "note_followers") == 0)
942            scope = SCOPE_FOLLOWERS;
943
944        msg = msg_note(&snac, content, NULL, in_reply_to, attl, scope, getenv("LANG"), post_date);
945
946        const char *id = xs_dict_get(msg, "id");
947
948        if (post_date)
949            schedule_add(&snac, id, msg);
950        else {
951            c_msg = msg_create(&snac, msg);
952
953            if (dbglevel) {
954                xs_json_dump(c_msg, 4, stdout);
955            }
956
957            enqueue_message(&snac, c_msg);
958            enqueue_webmention(msg);
959
960            timeline_add(&snac, id, msg);
961        }
962
963        return 0;
964    }
965
966    fprintf(stderr, "ERROR: bad command '%s'\n", cmd);
967
968    return 1;
969}