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