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