snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

main.c (23920B)


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