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