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}