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