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