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