activitypub.c (107001B)
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_json.h" 6 #include "xs_curl.h" 7 #include "xs_mime.h" 8 #include "xs_openssl.h" 9 #include "xs_regex.h" 10 #include "xs_time.h" 11 #include "xs_set.h" 12 #include "xs_match.h" 13 #include "xs_unicode.h" 14 #include "xs_webmention.h" 15 16 #include "snac.h" 17 18 #include <sys/wait.h> 19 #include <spawn.h> 20 21 extern char **environ; 22 23 const char *public_address = "https:/" "/www.w3.org/ns/activitystreams#Public"; 24 25 /* susie.png */ 26 27 const char *susie = 28 "iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAQAAAAC" 29 "CEkxzAAAAUUlEQVQoz43R0QkAMQwCUDdw/y3dwE" 30 "vsvzlL4X1IoQkAisKmwfAFT3RgJHbQezpSRoXEq" 31 "eqCL9BJBf7h3QbOCCxV5EVWMEMwG7K1/WODtlvx" 32 "AYTtEsDU9F34AAAAAElFTkSuQmCC"; 33 34 const char *susie_cool = 35 "iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAQAAAAC" 36 "CEkxzAAAAV0lEQVQoz43RwQ3AMAwCQDZg/y3ZgN" 37 "qo3+JaedwDOUQBQFHYaTB8wTM6sGl2cMPu+DFzn" 38 "+ZcgN7wF7ZVihXkfSlWIVzIA6dbQzaygllpNuTX" 39 "ZmmFNlvxADX1+o0cUPMbAAAAAElFTkSuQmCC"; 40 41 const char *susie_muertos = 42 "iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAQAAAAC" 43 "CEkxzAAAAV0lEQVQoz4XQsQ0AMQxCUW/A/lv+DT" 44 "ic6zGRolekIMyMELNp8PiCEw6Q4w4NoAt53IH5m" 45 "xXksrZYgZwJrIox+Z8vJAfe2lCxG6AK7eKkWcEb" 46 "QHbF617xAQatAAD7jJHUAAAAAElFTkSuQmCC"; 47 48 49 const char *default_avatar_base64(void) 50 /* returns the default avatar in base64 */ 51 { 52 time_t t = time(NULL); 53 struct tm tm; 54 const char *p = susie; 55 56 gmtime_r(&t, &tm); 57 58 if (tm.tm_mon == 10 && tm.tm_mday == 2) 59 p = susie_muertos; 60 else 61 if (tm.tm_wday == 0 || tm.tm_wday == 6) 62 p = susie_cool; 63 64 return p; 65 } 66 67 68 int activitypub_request(snac *user, const char *url, xs_dict **data) 69 /* request an object */ 70 { 71 int status = 0; 72 xs *response = NULL; 73 xs *payload = NULL; 74 int p_size; 75 const char *ctype; 76 77 *data = NULL; 78 79 if (user != NULL) { 80 /* get from the net */ 81 response = http_signed_request(user, "GET", url, 82 NULL, NULL, 0, &status, &payload, &p_size, 0); 83 } 84 85 if (status == 0 || (status >= 500 && status <= 599)) { 86 /* I found an instance running Misskey that returned 87 500 on signed messages but returned the object 88 perfectly without signing (?), so why not try */ 89 xs_free(response); 90 91 xs *hdrs = xs_dict_new(); 92 hdrs = xs_dict_append(hdrs, "accept", "application/activity+json"); 93 hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT); 94 95 response = xs_http_request("GET", url, hdrs, 96 NULL, 0, &status, &payload, &p_size, 0); 97 } 98 99 if (valid_status(status)) { 100 /* ensure it's ActivityPub data */ 101 ctype = xs_dict_get(response, "content-type"); 102 103 if (xs_is_null(ctype)) 104 status = HTTP_STATUS_BAD_REQUEST; 105 else 106 if (xs_str_in(ctype, "application/activity+json") != -1 || 107 xs_str_in(ctype, "application/ld+json") != -1) { 108 109 /* if there is no payload, fail */ 110 if (xs_is_null(payload)) 111 status = HTTP_STATUS_BAD_REQUEST; 112 else 113 *data = xs_json_loads(payload); 114 } 115 else 116 status = HTTP_STATUS_INTERNAL_SERVER_ERROR; 117 } 118 119 return status; 120 } 121 122 123 int actor_request(snac *user, const char *actor, xs_dict **data) 124 /* request an actor */ 125 { 126 int status; 127 xs *payload = NULL; 128 129 if (data) 130 *data = NULL; 131 132 /* get from disk first */ 133 status = actor_get_refresh(user, actor, data); 134 135 if (!valid_status(status)) { 136 /* actor data non-existent: get from the net */ 137 status = activitypub_request(user, actor, &payload); 138 139 if (valid_status(status)) { 140 /* renew data */ 141 status = actor_add(actor, payload); 142 143 if (data != NULL) { 144 *data = payload; 145 payload = NULL; 146 } 147 } 148 else 149 srv_debug(1, xs_fmt("actor_request error %s %d", actor, status)); 150 } 151 152 /* collect the (presumed) shared inbox in this actor */ 153 if (xs_type(xs_dict_get(srv_config, "disable_inbox_collection")) != XSTYPE_TRUE) { 154 if (valid_status(status) && data && *data) 155 inbox_add_by_actor(*data); 156 } 157 158 return status; 159 } 160 161 162 const char *get_atto(const xs_dict *msg) 163 /* gets the attributedTo field (an actor) */ 164 { 165 const xs_val *actor = xs_dict_get(msg, "attributedTo"); 166 167 /* if the actor is a list of objects (like on Peertube videos), pick the Person */ 168 if (xs_type(actor) == XSTYPE_LIST) { 169 const xs_list *p = actor; 170 int c = 0; 171 const xs_dict *v; 172 actor = NULL; 173 174 while (actor == NULL && xs_list_next(p, &v, &c)) { 175 if (xs_type(v) == XSTYPE_DICT) { 176 const char *type = xs_dict_get(v, "type"); 177 if (xs_type(type) == XSTYPE_STRING && strcmp(type, "Person") == 0) { 178 actor = xs_dict_get(v, "id"); 179 180 if (xs_type(actor) != XSTYPE_STRING) 181 actor = NULL; 182 } 183 } 184 } 185 } 186 else 187 if (xs_type(actor) == XSTYPE_DICT) { 188 /* bandwagon.fm returns this */ 189 actor = xs_dict_get(actor, "id"); 190 } 191 192 return actor; 193 } 194 195 196 const char *get_in_reply_to(const xs_dict *msg) 197 /* gets the inReplyTo id */ 198 { 199 const xs_val *in_reply_to = xs_dict_get(msg, "inReplyTo"); 200 201 if (xs_type(in_reply_to) == XSTYPE_DICT) 202 in_reply_to = xs_dict_get(in_reply_to, "id"); 203 204 return in_reply_to; 205 } 206 207 208 xs_list *get_attachments(const xs_dict *msg) 209 /* unify the garbage fire that are the attachments */ 210 { 211 xs_list *l = xs_list_new(); 212 const xs_list *p; 213 214 /* try first the attachments list */ 215 if (!xs_is_null(p = xs_dict_get(msg, "attachment"))) { 216 xs *attach = NULL; 217 const xs_val *v; 218 219 /* ensure it's a list */ 220 if (xs_type(p) == XSTYPE_DICT) { 221 attach = xs_list_new(); 222 attach = xs_list_append(attach, p); 223 } 224 else 225 attach = xs_dup(p); 226 227 if (xs_type(attach) == XSTYPE_LIST && xs_list_len(attach) == 0) { 228 /* does the message have an image? */ 229 const xs_dict *d = xs_dict_get(msg, "image"); 230 if (xs_type(d) == XSTYPE_DICT) { 231 /* add it to the attachment list */ 232 attach = xs_list_append(attach, d); 233 } 234 } 235 236 /* now iterate the list */ 237 int c = 0; 238 while (xs_list_next(attach, &v, &c)) { 239 const char *type = xs_dict_get(v, "mediaType"); 240 if (xs_is_null(type)) 241 type = xs_dict_get(v, "type"); 242 243 if (xs_is_null(type)) 244 continue; 245 246 const char *href = xs_dict_get(v, "url"); 247 if (xs_is_null(href)) 248 href = xs_dict_get(v, "href"); 249 if (xs_is_null(href)) 250 continue; 251 252 /* infer MIME type from non-specific attachments */ 253 if (xs_list_len(attach) < 2 && xs_match(type, "Link|Document")) { 254 char *mt = (char *)xs_mime_by_ext(href); 255 256 if (xs_match(mt, "image/*|audio/*|video/*")) /* */ 257 type = mt; 258 } 259 260 const char *name = xs_dict_get(v, "name"); 261 if (xs_is_null(name)) 262 name = xs_dict_get(msg, "name"); 263 if (xs_is_null(name)) 264 name = ""; 265 266 xs *d = xs_dict_new(); 267 d = xs_dict_append(d, "type", type); 268 d = xs_dict_append(d, "href", href); 269 d = xs_dict_append(d, "name", name); 270 271 const xs_dict *icon = xs_dict_get(v, "icon"); 272 if (xs_type(icon) == XSTYPE_DICT) 273 d = xs_dict_append(d, "icon", icon); 274 275 l = xs_list_append(l, d); 276 } 277 } 278 279 /** urls (attachments from Peertube) **/ 280 p = xs_dict_get(msg, "url"); 281 282 if (xs_type(p) == XSTYPE_LIST) { 283 const char *href = NULL; 284 const char *type = NULL; 285 int c = 0; 286 const xs_val *v; 287 288 while (href == NULL && xs_list_next(p, &v, &c)) { 289 if (xs_type(v) == XSTYPE_DICT) { 290 const char *mtype = xs_dict_get(v, "type"); 291 292 if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "Link") == 0) { 293 mtype = xs_dict_get(v, "mediaType"); 294 const xs_list *tag = xs_dict_get(v, "tag"); 295 296 if (xs_type(mtype) == XSTYPE_STRING && 297 strcmp(mtype, "application/x-mpegURL") == 0 && 298 xs_type(tag) == XSTYPE_LIST) { 299 /* now iterate the tag list, looking for a video URL */ 300 const xs_dict *d; 301 int c = 0; 302 303 while (href == NULL && xs_list_next(tag, &d, &c)) { 304 if (xs_type(d) == XSTYPE_DICT) { 305 if (xs_type(mtype = xs_dict_get(d, "mediaType")) == XSTYPE_STRING && 306 xs_startswith(mtype, "video/")) { 307 const char *h = xs_dict_get(d, "href"); 308 309 /* this is probably it */ 310 if (xs_type(h) == XSTYPE_STRING) { 311 href = h; 312 type = mtype; 313 } 314 } 315 } 316 } 317 } 318 } 319 } 320 } 321 322 if (href && type) { 323 xs *d = xs_dict_new(); 324 d = xs_dict_append(d, "href", href); 325 d = xs_dict_append(d, "type", type); 326 d = xs_dict_append(d, "name", "---"); 327 328 l = xs_list_append(l, d); 329 } 330 } 331 332 return l; 333 } 334 335 336 int hashtag_in_msg(const xs_list *hashtags, const xs_dict *msg) 337 /* returns 1 if the message contains any of the list of hashtags */ 338 { 339 if (xs_is_list(hashtags) && xs_is_dict(msg)) { 340 const xs_list *tags_in_msg = xs_dict_get(msg, "tag"); 341 342 if (xs_is_list(tags_in_msg)) { 343 const xs_dict *te; 344 345 /* iterate the tags in the message */ 346 xs_list_foreach(tags_in_msg, te) { 347 if (xs_is_dict(te)) { 348 const char *type = xs_dict_get(te, "type"); 349 const char *name = xs_dict_get(te, "name"); 350 351 if (xs_is_string(type) && xs_is_string(name)) { 352 if (strcmp(type, "Hashtag") == 0) { 353 xs *lc_name = xs_utf8_to_lower(name); 354 355 if (xs_list_in(hashtags, lc_name) != -1) 356 return 1; 357 } 358 } 359 } 360 } 361 } 362 } 363 364 return 0; 365 } 366 367 368 int followed_hashtag_check(snac *user, const xs_dict *msg) 369 /* returns 1 if this message contains a hashtag followed by me */ 370 { 371 return hashtag_in_msg(xs_dict_get(user->config, "followed_hashtags"), msg); 372 } 373 374 375 int blocked_hashtag_check(snac *user, const xs_dict *msg) 376 /* returns 1 if this message contains a hashtag blocked by me */ 377 { 378 return hashtag_in_msg(xs_dict_get(user->config, "blocked_hashtags"), msg); 379 } 380 381 382 int timeline_request(snac *snac, const char **id, xs_str **wrk, int level) 383 /* ensures that an entry and its ancestors are in the timeline */ 384 { 385 int status = 0; 386 387 if (level < MAX_CONVERSATION_LEVELS && !xs_is_null(*id)) { 388 xs *msg = NULL; 389 390 /* from a blocked instance? discard and break */ 391 if (is_instance_blocked(*id)) { 392 snac_debug(snac, 1, xs_fmt("timeline_request blocked instance %s", *id)); 393 return status; 394 } 395 396 /* is the object already there? */ 397 if (!valid_status((status = object_get(*id, &msg)))) { 398 /* no; download it */ 399 status = activitypub_request(snac, *id, &msg); 400 } 401 402 if (valid_status(status)) { 403 const xs_dict *object = msg; 404 const char *type = xs_dict_get(object, "type"); 405 406 /* get the id again from the object, as it may be different */ 407 const char *nid = xs_dict_get(object, "id"); 408 409 if (xs_type(nid) != XSTYPE_STRING) 410 return 0; 411 412 if (wrk && strcmp(nid, *id) != 0) { 413 snac_debug(snac, 1, 414 xs_fmt("timeline_request canonical id for %s is %s", *id, nid)); 415 416 *wrk = xs_dup(nid); 417 *id = *wrk; 418 } 419 420 if (xs_is_null(type)) 421 type = "(null)"; 422 423 srv_debug(2, xs_fmt("timeline_request type %s '%s'", nid, type)); 424 425 if (strcmp(type, "Create") == 0) { 426 /* some software like lemmy nest Announce + Create + Note */ 427 if (!xs_is_null(object = xs_dict_get(object, "object"))) { 428 type = xs_dict_get(object, "type"); 429 nid = xs_dict_get(object, "id"); 430 } 431 else 432 type = "(null)"; 433 } 434 435 if (xs_match(type, POSTLIKE_OBJECT_TYPE)) { 436 if (content_match("filter_reject.txt", object)) 437 snac_log(snac, xs_fmt("timeline_request rejected by content %s", nid)); 438 else 439 if (blocked_hashtag_check(snac, object)) 440 snac_log(snac, xs_fmt("timeline_request rejected by hashtag %s", nid)); 441 else { 442 const char *actor = get_atto(object); 443 444 if (!xs_is_null(actor)) { 445 /* request (and drop) the actor for this entry */ 446 if (!valid_status(actor_request(snac, actor, NULL))) { 447 /* failed? retry later */ 448 enqueue_actor_refresh(snac, actor, 60); 449 } 450 451 /* does it have an ancestor? */ 452 const char *in_reply_to = get_in_reply_to(object); 453 454 /* store */ 455 timeline_add(snac, nid, object); 456 457 /* redistribute to lists for this user */ 458 list_distribute(snac, actor, object); 459 460 /* recurse! */ 461 timeline_request(snac, &in_reply_to, NULL, level + 1); 462 } 463 } 464 } 465 } 466 } 467 468 return status; 469 } 470 471 472 int send_to_inbox_raw(const char *keyid, const char *seckey, 473 const xs_str *inbox, const xs_dict *msg, 474 xs_val **payload, int *p_size, int timeout) 475 /* sends a message to an Inbox */ 476 { 477 int status; 478 xs_dict *response; 479 xs *j_msg = xs_json_dumps((xs_dict *)msg, 4); 480 481 response = http_signed_request_raw(keyid, seckey, "POST", inbox, 482 NULL, j_msg, strlen(j_msg), &status, payload, p_size, timeout); 483 484 xs_free(response); 485 486 return status; 487 } 488 489 490 int send_to_inbox(snac *snac, const xs_str *inbox, const xs_dict *msg, 491 xs_val **payload, int *p_size, int timeout) 492 /* sends a message to an Inbox */ 493 { 494 const char *seckey = xs_dict_get(snac->key, "secret"); 495 496 return send_to_inbox_raw(snac->actor, seckey, inbox, msg, payload, p_size, timeout); 497 } 498 499 500 xs_str *get_actor_inbox(const char *actor, int shared) 501 /* gets an actor's inbox */ 502 { 503 xs *data = NULL; 504 const char *v = NULL; 505 506 if (valid_status(actor_request(NULL, actor, &data))) { 507 /* try first endpoints/sharedInbox */ 508 if (shared && (v = xs_dict_get(data, "endpoints"))) 509 v = xs_dict_get(v, "sharedInbox"); 510 511 /* try then the regular inbox */ 512 if (xs_is_null(v)) 513 v = xs_dict_get(data, "inbox"); 514 } 515 516 return xs_is_null(v) ? NULL : xs_dup(v); 517 } 518 519 520 int send_to_actor(snac *snac, const char *actor, const xs_dict *msg, 521 xs_val **payload, int *p_size, int timeout) 522 /* sends a message to an actor */ 523 { 524 int status = HTTP_STATUS_BAD_REQUEST; 525 xs *inbox = get_actor_inbox(actor, 1); 526 527 if (!xs_is_null(inbox)) 528 status = send_to_inbox(snac, inbox, msg, payload, p_size, timeout); 529 530 return status; 531 } 532 533 534 void post_message(snac *snac, const char *actor, const xs_dict *msg) 535 /* posts a message immediately (bypassing the output queues) */ 536 { 537 xs *payload = NULL; 538 int p_size; 539 540 int status = send_to_actor(snac, actor, msg, &payload, &p_size, 3); 541 542 srv_log(xs_fmt("post_message to actor %s %d", actor, status)); 543 544 if (!valid_status(status)) 545 /* cannot send right now, enqueue */ 546 enqueue_message(snac, msg); 547 } 548 549 550 xs_list *recipient_list(snac *snac, const xs_dict *msg, int expand_public) 551 /* returns the list of recipients for a message */ 552 { 553 const xs_val *to = xs_dict_get(msg, "to"); 554 const xs_val *cc = xs_dict_get(msg, "cc"); 555 xs_set rcpts; 556 int n; 557 558 xs_set_init(&rcpts); 559 560 const xs_list *lists[] = { to, cc, NULL }; 561 for (n = 0; lists[n]; n++) { 562 xs_list *l = (xs_list *)lists[n]; 563 const char *v; 564 xs *tl = NULL; 565 566 /* if it's a string, create a list with only one element */ 567 if (xs_type(l) == XSTYPE_STRING) { 568 tl = xs_list_new(); 569 tl = xs_list_append(tl, l); 570 571 l = tl; 572 } 573 574 while (xs_list_iter(&l, &v)) { 575 if (expand_public && strcmp(v, public_address) == 0) { 576 /* iterate the followers and add them */ 577 xs *fwers = follower_list(snac); 578 const char *actor; 579 580 char *p = fwers; 581 while (xs_list_iter(&p, &actor)) 582 xs_set_add(&rcpts, actor); 583 } 584 else 585 xs_set_add(&rcpts, v); 586 } 587 } 588 589 return xs_set_result(&rcpts); 590 } 591 592 593 int is_msg_public(const xs_dict *msg) 594 /* checks if a message is public */ 595 { 596 const char *to = xs_dict_get(msg, "to"); 597 const char *cc = xs_dict_get(msg, "cc"); 598 int n; 599 600 const char *lists[] = { to, cc, NULL }; 601 for (n = 0; lists[n]; n++) { 602 const xs_val *l = lists[n]; 603 604 if (xs_type(l) == XSTYPE_STRING) { 605 if (strcmp(l, public_address) == 0) 606 return 1; 607 } 608 else 609 if (xs_type(l) == XSTYPE_LIST) { 610 if (xs_list_in(l, public_address) != -1) 611 return 1; 612 } 613 } 614 615 return 0; 616 } 617 618 619 int is_msg_from_private_user(const xs_dict *msg) 620 /* checks if a message is from a local, private user */ 621 { 622 int ret = 0; 623 624 /* is this message from a local user? */ 625 if (xs_startswith(xs_dict_get(msg, "id"), srv_baseurl)) { 626 const char *atto = get_atto(msg); 627 xs *l = xs_split(atto, "/"); 628 const char *uid = xs_list_get(l, -1); 629 snac user; 630 631 if (uid && user_open(&user, uid)) { 632 if (xs_type(xs_dict_get(user.config, "private")) == XSTYPE_TRUE) 633 ret = 1; 634 635 user_free(&user); 636 } 637 } 638 639 return ret; 640 } 641 642 643 void followed_hashtag_distribute(const xs_dict *msg) 644 /* distribute this post to all users following the included hashtags */ 645 { 646 const char *id = xs_dict_get(msg, "id"); 647 const xs_list *tags_in_msg = xs_dict_get(msg, "tag"); 648 649 if (!xs_is_string(id) || !xs_is_list(tags_in_msg) || xs_list_len(tags_in_msg) == 0) 650 return; 651 652 srv_debug(1, xs_fmt("followed_hashtag_distribute check for %s", id)); 653 654 xs *users = user_list(); 655 const char *uid; 656 657 xs_list_foreach(users, uid) { 658 snac user; 659 660 if (user_open(&user, uid)) { 661 if (followed_hashtag_check(&user, msg)) { 662 timeline_add(&user, id, msg); 663 664 snac_log(&user, xs_fmt("followed hashtag in %s", id)); 665 } 666 667 user_free(&user); 668 } 669 } 670 } 671 672 673 int is_msg_for_me(snac *snac, const xs_dict *c_msg) 674 /* checks if this message is for me */ 675 { 676 const char *type = xs_dict_get(c_msg, "type"); 677 const char *actor = xs_dict_get(c_msg, "actor"); 678 679 if (strcmp(actor, snac->actor) == 0) { 680 /* message by myself? (most probably via the shared-inbox) reject */ 681 snac_debug(snac, 1, xs_fmt("ignoring message by myself")); 682 return 0; 683 } 684 685 if (xs_match(type, "Like|Announce|EmojiReact")) { 686 const char *object = xs_dict_get(c_msg, "object"); 687 xs *obj = NULL; 688 689 if (xs_is_dict(object)) { 690 obj = xs_dup(object); 691 object = xs_dict_get(object, "id"); 692 } 693 694 /* bad object id? reject */ 695 if (!xs_is_string(object)) 696 return 0; 697 698 /* try to get the object */ 699 if (!xs_is_dict(obj)) 700 object_get(object, &obj); 701 702 /* if it's about one of our posts, accept it */ 703 if (xs_startswith(object, snac->actor)) 704 return 2; 705 706 /* blocked by hashtag? */ 707 if (blocked_hashtag_check(snac, obj)) 708 return 0; 709 710 /* if it's by someone we follow, accept it */ 711 if (following_check(snac, actor)) 712 return 1; 713 714 /* do we follow any hashtag? */ 715 if (followed_hashtag_check(snac, obj)) 716 return 7; 717 718 return 0; 719 } 720 721 /* if it's an Undo, it must be from someone related to us */ 722 if (xs_match(type, "Undo")) { 723 return follower_check(snac, actor) || following_check(snac, actor); 724 } 725 726 /* if it's an Accept + Follow, it must be for a Follow we created */ 727 if (xs_match(type, "Accept")) { 728 return following_check(snac, actor); 729 } 730 731 /* if it's a Follow, it must be explicitly for us */ 732 if (xs_match(type, "Follow")) { 733 const char *object = xs_dict_get(c_msg, "object"); 734 return !xs_is_null(object) && strcmp(snac->actor, object) == 0; 735 } 736 737 /* only accept Ping directed to us */ 738 if (xs_match(type, "Ping")) { 739 const char *dest = xs_dict_get(c_msg, "to"); 740 return !xs_is_null(dest) && strcmp(snac->actor, dest) == 0; 741 } 742 743 /* if it's not a Create or Update, allow as is */ 744 if (!xs_match(type, "Create|Update")) { 745 return 1; 746 } 747 748 const xs_dict *msg = xs_dict_get(c_msg, "object"); 749 750 /* any blocked hashtag? reject */ 751 if (blocked_hashtag_check(snac, msg)) { 752 snac_debug(snac, 1, xs_fmt("blocked by hashtag %s", xs_dict_get(msg, "id"))); 753 return 0; 754 } 755 756 int pub_msg = is_msg_public(c_msg); 757 758 /* if this message is public and we follow the actor of this post, allow */ 759 if (pub_msg && following_check(snac, actor)) 760 return 1; 761 762 xs *rcpts = recipient_list(snac, msg, 0); 763 xs_list *p = rcpts; 764 const xs_str *v; 765 766 xs *actor_followers = NULL; 767 768 if (!pub_msg) { 769 /* not a public message; get the actor and its followers list */ 770 xs *actor_obj = NULL; 771 772 if (valid_status(object_get(actor, &actor_obj))) { 773 const xs_val *fw = xs_dict_get(actor_obj, "followers"); 774 if (fw) 775 actor_followers = xs_dup(fw); 776 } 777 } 778 779 while(xs_list_iter(&p, &v)) { 780 /* explicitly for me? accept */ 781 if (strcmp(v, snac->actor) == 0) 782 return 2; 783 784 if (pub_msg) { 785 /* a public message for someone we follow? (probably cc'ed) accept */ 786 if (strcmp(v, public_address) != 0 && following_check(snac, v)) 787 return 5; 788 } 789 else 790 if (actor_followers && strcmp(v, actor_followers) == 0) { 791 /* if this message is for this actor's followers, are we one of them? */ 792 if (following_check(snac, actor)) 793 return 6; 794 } 795 } 796 797 /* accept if it's by someone we follow */ 798 const char *atto = get_atto(msg); 799 800 if (pub_msg && !xs_is_null(atto) && following_check(snac, atto)) 801 return 3; 802 803 /* is this message a reply to another? */ 804 const char *irt = get_in_reply_to(msg); 805 if (!xs_is_null(irt)) { 806 xs *r_msg = NULL; 807 808 /* try to get the replied message */ 809 if (valid_status(object_get(irt, &r_msg))) { 810 atto = get_atto(r_msg); 811 812 /* accept if the replied message is from someone we follow */ 813 if (pub_msg && !xs_is_null(atto) && following_check(snac, atto)) 814 return 4; 815 } 816 } 817 818 /* does this message contain a tag we are following? */ 819 if (pub_msg && followed_hashtag_check(snac, msg)) 820 return 7; 821 822 return 0; 823 } 824 825 826 xs_str *process_tags(snac *snac, const char *content, xs_list **tag) 827 /* parses mentions and tags from content */ 828 { 829 xs_str *nc = xs_str_new(NULL); 830 xs_list *tl = *tag; 831 xs *split; 832 xs_list *p; 833 const xs_val *v; 834 int n = 0; 835 836 /* create a default server for incomplete mentions */ 837 xs *def_srv = NULL; 838 839 if (xs_list_len(tl)) { 840 /* if there are any mentions, get the server from 841 the first one, which is the inReplyTo author */ 842 p = tl; 843 while (xs_list_iter(&p, &v)) { 844 const char *type = xs_dict_get(v, "type"); 845 const char *name = xs_dict_get(v, "name"); 846 847 if (type && name && strcmp(type, "Mention") == 0) { 848 xs *l = xs_split(name, "@"); 849 850 def_srv = xs_dup(xs_list_get(l, -1)); 851 852 break; 853 } 854 } 855 } 856 857 if (xs_is_null(def_srv)) 858 /* use this same server */ 859 def_srv = xs_dup(xs_dict_get(srv_config, "host")); 860 861 split = xs_regex_split(content, "(@[A-Za-z0-9_]+(@[A-Za-z0-9\\.-]+)?|&#[0-9]+;|#(_|[^[:punct:][:space:]])+)"); 862 863 p = split; 864 while (xs_list_iter(&p, &v)) { 865 if ((n & 0x1)) { 866 if (*v == '@') { 867 xs *link = NULL; 868 xs *wuid = NULL; 869 870 if (strchr(v + 1, '@') == NULL) { 871 /* only one @? it's a dumb Mastodon-like mention 872 without server; add the default one */ 873 wuid = xs_fmt("%s@%s", v, def_srv); 874 875 snac_debug(snac, 2, xs_fmt("mention without server '%s' '%s'", v, wuid)); 876 } 877 else 878 wuid = xs_dup(v); 879 880 /* query the webfinger about this fellow */ 881 xs *actor = NULL; 882 xs *uid = NULL; 883 int status; 884 885 status = webfinger_request(wuid, &actor, &uid); 886 887 if (valid_status(status) && actor && uid) { 888 xs *d = xs_dict_new(); 889 890 d = xs_dict_append(d, "type", "Mention"); 891 d = xs_dict_append(d, "href", actor); 892 d = xs_dict_set_fmt(d, "name", "@%s", uid); 893 894 tl = xs_list_append(tl, d); 895 896 nc = xs_str_cat_fmt(nc, "<span class=\"h-card\"><a href=\"%s\" class=\"u-url mention\">@%s</a></span>", actor, uid); 897 } 898 else 899 nc = xs_str_cat(nc, v); 900 } 901 else 902 if (*v == '#') { 903 /* hashtag */ 904 xs *d = xs_dict_new(); 905 xs *n = xs_utf8_to_lower(v); 906 907 d = xs_dict_append(d, "type", "Hashtag"); 908 d = xs_dict_set_fmt(d, "href", "%s?t=%s", srv_baseurl, n + 1); 909 d = xs_dict_append(d, "name", n); 910 911 tl = xs_list_append(tl, d); 912 913 /* add the code */ 914 nc = xs_str_cat_fmt(nc, "<a href=\"%s?t=%s\" class=\"mention hashtag\" rel=\"tag\">%s</a>", srv_baseurl, n + 1, v); 915 } 916 else 917 if (*v == '&') { 918 /* HTML Unicode entity, probably part of an emoji */ 919 920 /* write as is */ 921 nc = xs_str_cat(nc, v); 922 } 923 } 924 else 925 nc = xs_str_cat(nc, v); 926 927 n++; 928 } 929 930 *tag = tl; 931 932 return nc; 933 } 934 935 936 void notify(snac *snac, const char *type, const char *utype, const char *actor, const xs_dict *msg) 937 /* notifies the user of relevant events */ 938 { 939 /* skip our own notifications */ 940 if (strcmp(snac->actor, actor) == 0) 941 return; 942 943 const char *id = xs_dict_get(msg, "id"); 944 945 if (strcmp(type, "Create") == 0) { 946 /* only notify of notes specifically for us */ 947 xs *rcpts = recipient_list(snac, msg, 0); 948 949 if (xs_list_in(rcpts, snac->actor) == -1) 950 return; 951 952 /* discard votes */ 953 const xs_dict *note = xs_dict_get(msg, "object"); 954 955 if (note && !xs_is_null(xs_dict_get(note, "name"))) 956 return; 957 } 958 959 if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") != 0) 960 return; 961 962 /* get the object id */ 963 const char *objid = xs_dict_get(msg, "object"); 964 965 if (xs_type(objid) == XSTYPE_DICT) 966 objid = xs_dict_get(objid, "id"); 967 968 if (xs_match(type, "Like|Announce|EmojiReact")) { 969 /* if it's not an admiration about something by us, done */ 970 if (xs_is_null(objid) || !xs_startswith(objid, snac->actor)) 971 return; 972 973 /* if it's an announce by our own relay, done */ 974 if (xs_startswith(id, srv_baseurl) && 975 xs_startswith(id + strlen(srv_baseurl), "/relay")) 976 return; 977 } 978 979 /* updated poll? */ 980 if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0) { 981 const xs_dict *poll; 982 const char *poll_id; 983 984 if ((poll = xs_dict_get(msg, "object")) == NULL) 985 return; 986 987 /* if it's not closed, discard */ 988 if (xs_is_null(xs_dict_get(poll, "closed"))) 989 return; 990 991 if ((poll_id = xs_dict_get(poll, "id")) == NULL) 992 return; 993 994 /* if it's not ours and we didn't vote, discard */ 995 if (!xs_startswith(poll_id, snac->actor) && !was_question_voted(snac, poll_id)) 996 return; 997 } 998 999 /* user will love to know about this! */ 1000 1001 /* prepare message body */ 1002 xs *body = xs_fmt("User : @%s@%s\n", 1003 xs_dict_get(snac->config, "uid"), 1004 xs_dict_get(srv_config, "host") 1005 ); 1006 1007 if (strcmp(utype, "(null)") != 0) { 1008 body = xs_str_cat_fmt(body, "Type : %s + %s\n", type, utype); 1009 } 1010 else { 1011 body = xs_str_cat_fmt(body, "Type : %s\n", type); 1012 } 1013 1014 body = xs_str_cat_fmt(body, "Actor: %s\n", actor); 1015 1016 if (objid != NULL) { 1017 body = xs_str_cat_fmt(body, "Object: %s\n", objid); 1018 } 1019 1020 /* email */ 1021 1022 const char *email = "[disabled by admin]"; 1023 1024 if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) { 1025 email = xs_dict_get(snac->config_o, "email"); 1026 if (xs_is_null(email)) { 1027 email = xs_dict_get(snac->config, "email"); 1028 1029 if (xs_is_null(email)) 1030 email = "[empty]"; 1031 } 1032 } 1033 1034 if (*email != '\0' && *email != '[') { 1035 snac_debug(snac, 1, xs_fmt("email notify %s %s %s", type, utype, actor)); 1036 1037 xs *subject = xs_fmt("snac notify for @%s@%s", 1038 xs_dict_get(snac->config, "uid"), xs_dict_get(srv_config, "host")); 1039 xs *from = xs_fmt("<snac-daemon@%s>", xs_dict_get(srv_config, "host")); 1040 xs *header = xs_fmt( 1041 "From: snac-daemon %s\n" 1042 "To: %s\n" 1043 "Subject: %s\n" 1044 "\n", 1045 from, email, subject); 1046 1047 xs *mailinfo = xs_dict_new(); 1048 xs *bd = xs_fmt("%s%s", header, body); 1049 mailinfo = xs_dict_append(mailinfo, "from", from); 1050 mailinfo = xs_dict_append(mailinfo, "to", email); 1051 mailinfo = xs_dict_append(mailinfo, "body", bd); 1052 1053 enqueue_email(mailinfo, 0); 1054 } 1055 1056 /* telegram */ 1057 1058 const char *bot = xs_dict_get(snac->config, "telegram_bot"); 1059 const char *chat_id = xs_dict_get(snac->config, "telegram_chat_id"); 1060 1061 if (!xs_is_null(bot) && !xs_is_null(chat_id) && *bot && *chat_id) 1062 enqueue_telegram(body, bot, chat_id); 1063 1064 /* ntfy */ 1065 const char *ntfy_server = xs_dict_get(snac->config, "ntfy_server"); 1066 const char *ntfy_token = xs_dict_get(snac->config, "ntfy_token"); 1067 1068 if (!xs_is_null(ntfy_server) && *ntfy_server) 1069 enqueue_ntfy(body, ntfy_server, ntfy_token); 1070 1071 /* auto boost */ 1072 if (xs_match(type, "Create") && xs_is_true(xs_dict_get(snac->config, "auto_boost"))) { 1073 xs *msg = msg_admiration(snac, objid, "Announce"); 1074 enqueue_message(snac, msg); 1075 1076 snac_debug(snac, 1, xs_fmt("auto boosted %s", objid)); 1077 } 1078 1079 /* finally, store it in the notification folder */ 1080 if (strcmp(type, "Follow") == 0) 1081 objid = id; 1082 else 1083 if (strcmp(utype, "Follow") == 0) 1084 objid = actor; 1085 1086 notify_add(snac, type, utype, actor, objid != NULL ? objid : id, msg); 1087 } 1088 1089 /** messages **/ 1090 1091 xs_dict *msg_base(snac *snac, const char *type, const char *id, 1092 const char *actor, const char *date, const char *object) 1093 /* creates a base ActivityPub message */ 1094 { 1095 xs *did = NULL; 1096 xs *published = NULL; 1097 xs *ntid = tid(0); 1098 const char *obj_id; 1099 1100 if (xs_type(object) == XSTYPE_DICT) 1101 obj_id = xs_dict_get(object, "id"); 1102 else 1103 obj_id = object; 1104 1105 /* generated values */ 1106 if (date && strcmp(date, "@now") == 0) { 1107 published = xs_str_utctime(0, ISO_DATE_SPEC); 1108 date = published; 1109 } 1110 1111 if (id != NULL) { 1112 if (strcmp(id, "@dummy") == 0) { 1113 did = xs_fmt("%s/d/%s/%s", snac->actor, ntid, type); 1114 1115 id = did; 1116 } 1117 else 1118 if (strcmp(id, "@object") == 0) { 1119 if (obj_id != NULL) { 1120 did = xs_fmt("%s/%s_%s", obj_id, type, ntid); 1121 id = did; 1122 } 1123 else 1124 id = NULL; 1125 } 1126 else 1127 if (strcmp(id, "@wrapper") == 0) { 1128 /* like @object, but always generate the same id */ 1129 if (object != NULL) { 1130 date = xs_dict_get(object, "published"); 1131 did = xs_fmt("%s/%s", obj_id, type); 1132 id = did; 1133 } 1134 else 1135 id = NULL; 1136 } 1137 } 1138 1139 xs_dict *msg = xs_dict_new(); 1140 1141 msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams"); 1142 msg = xs_dict_append(msg, "type", type); 1143 1144 if (id != NULL) 1145 msg = xs_dict_append(msg, "id", id); 1146 1147 if (actor != NULL) 1148 msg = xs_dict_append(msg, "actor", actor); 1149 1150 if (date != NULL) 1151 msg = xs_dict_append(msg, "published", date); 1152 1153 if (object != NULL) 1154 msg = xs_dict_append(msg, "object", object); 1155 1156 return msg; 1157 } 1158 1159 1160 xs_dict *msg_collection(snac *snac, const char *id, int items) 1161 /* creates an empty OrderedCollection message */ 1162 { 1163 xs_dict *msg = msg_base(snac, "OrderedCollection", id, NULL, NULL, NULL); 1164 xs *n = xs_number_new(items); 1165 1166 msg = xs_dict_append(msg, "attributedTo", snac->actor); 1167 msg = xs_dict_append(msg, "totalItems", n); 1168 1169 return msg; 1170 } 1171 1172 1173 xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to) 1174 /* creates an Accept message (as a response to a Follow) */ 1175 { 1176 xs_dict *msg = msg_base(snac, "Accept", "@dummy", snac->actor, NULL, object); 1177 1178 msg = xs_dict_append(msg, "to", to); 1179 1180 return msg; 1181 } 1182 1183 1184 xs_dict *msg_update(snac *snac, const xs_dict *object) 1185 /* creates an Update message */ 1186 { 1187 xs_dict *msg = msg_base(snac, "Update", "@object", snac->actor, "@now", object); 1188 1189 const char *type = xs_dict_get(object, "type"); 1190 1191 if (strcmp(type, "Note") == 0) { 1192 msg = xs_dict_append(msg, "to", xs_dict_get(object, "to")); 1193 msg = xs_dict_append(msg, "cc", xs_dict_get(object, "cc")); 1194 } 1195 else 1196 if (strcmp(type, "Person") == 0) { 1197 msg = xs_dict_append(msg, "to", public_address); 1198 1199 /* also spam the people being followed, so that 1200 they have the newest information about who we are */ 1201 xs *cc = following_list(snac); 1202 1203 msg = xs_dict_append(msg, "cc", cc); 1204 } 1205 else 1206 msg = xs_dict_append(msg, "to", public_address); 1207 1208 return msg; 1209 } 1210 1211 1212 xs_dict *msg_admiration(snac *snac, const char *object, const char *type) 1213 /* creates a Like or Announce message */ 1214 { 1215 xs *a_msg = NULL; 1216 xs_dict *msg = NULL; 1217 xs *wrk = NULL; 1218 1219 /* call the object */ 1220 timeline_request(snac, &object, &wrk, 0); 1221 1222 if (valid_status(object_get(object, &a_msg))) { 1223 xs *rcpts = xs_list_new(); 1224 const char *o_md5 = xs_md5(object); 1225 xs *id = xs_fmt("%s/%s/%s", snac->actor, *type == 'L' ? "l" : "a", o_md5); 1226 1227 msg = msg_base(snac, type, id, snac->actor, "@now", object); 1228 1229 if (is_msg_public(a_msg)) 1230 rcpts = xs_list_append(rcpts, public_address); 1231 1232 rcpts = xs_list_append(rcpts, get_atto(a_msg)); 1233 1234 msg = xs_dict_append(msg, "to", rcpts); 1235 } 1236 else 1237 snac_log(snac, xs_fmt("msg_admiration cannot retrieve object %s", object)); 1238 1239 return msg; 1240 } 1241 1242 1243 xs_dict *msg_repulsion(snac *user, const char *id, const char *type) 1244 /* creates an Undo + admiration message */ 1245 { 1246 xs *a_msg = NULL; 1247 xs_dict *msg = NULL; 1248 1249 if (valid_status(object_get(id, &a_msg))) { 1250 /* create a clone of the original admiration message */ 1251 xs *object = msg_admiration(user, id, type); 1252 1253 /* delete the published date */ 1254 object = xs_dict_del(object, "published"); 1255 1256 /* create an undo message for this object */ 1257 msg = msg_undo(user, object); 1258 1259 /* copy the 'to' field */ 1260 msg = xs_dict_set(msg, "to", xs_dict_get(object, "to")); 1261 } 1262 1263 /* now we despise this */ 1264 object_unadmire(id, user->actor, *type == 'L' ? 1 : 0); 1265 1266 return msg; 1267 } 1268 1269 1270 xs_dict *msg_actor_place(snac *user, const char *label) 1271 /* creates a Place object, if the user has a location defined */ 1272 { 1273 xs_dict *place = NULL; 1274 const char *latitude = xs_dict_get_def(user->config, "latitude", ""); 1275 const char *longitude = xs_dict_get_def(user->config, "longitude", ""); 1276 1277 if (*latitude && *longitude) { 1278 xs *d_la = xs_number_new(atof(latitude)); 1279 xs *d_lo = xs_number_new(atof(longitude)); 1280 1281 place = msg_base(user, "Place", NULL, user->actor, NULL, NULL); 1282 1283 place = xs_dict_set(place, "name", label); 1284 place = xs_dict_set(place, "latitude", d_la); 1285 place = xs_dict_set(place, "longitude", d_lo); 1286 } 1287 1288 return place; 1289 } 1290 1291 1292 xs_dict *msg_actor(snac *snac) 1293 /* create a Person message for this actor */ 1294 { 1295 xs *ctxt = xs_list_new(); 1296 xs *icon = xs_dict_new(); 1297 xs *keys = xs_dict_new(); 1298 xs *tags = xs_list_new(); 1299 xs *avtr = NULL; 1300 xs *kid = NULL; 1301 xs *f_bio = NULL; 1302 xs_dict *msg = NULL; 1303 const char *p; 1304 int n; 1305 1306 /* everybody loves some caching */ 1307 if (time(NULL) - object_mtime(snac->actor) < 3 * 3600 && 1308 valid_status(object_get(snac->actor, &msg))) { 1309 snac_debug(snac, 2, xs_fmt("Returning cached actor %s", snac->actor)); 1310 1311 return msg; 1312 } 1313 1314 msg = msg_base(snac, "Person", snac->actor, NULL, NULL, NULL); 1315 1316 /* change the @context (is this really necessary?) */ 1317 ctxt = xs_list_append(ctxt, "https:/" "/www.w3.org/ns/activitystreams"); 1318 ctxt = xs_list_append(ctxt, "https:/" "/w3id.org/security/v1"); 1319 msg = xs_dict_set(msg, "@context", ctxt); 1320 1321 msg = xs_dict_set(msg, "url", snac->actor); 1322 msg = xs_dict_set(msg, "name", xs_dict_get(snac->config, "name")); 1323 msg = xs_dict_set(msg, "preferredUsername", snac->uid); 1324 msg = xs_dict_set(msg, "published", xs_dict_get(snac->config, "published")); 1325 1326 // this exists so we get the emoji tags from our name too. 1327 // and then we just throw away the result, because it's kinda useless to have markdown in the display name. 1328 xs *name_dummy = not_really_markdown(xs_dict_get(snac->config, "name"), NULL, &tags); 1329 1330 xs *f_bio_2 = not_really_markdown(xs_dict_get(snac->config, "bio"), NULL, &tags); 1331 f_bio = process_tags(snac, f_bio_2, &tags); 1332 msg = xs_dict_set(msg, "summary", f_bio); 1333 msg = xs_dict_set(msg, "tag", tags); 1334 1335 char *folders[] = { "inbox", "outbox", "followers", "following", "featured", NULL }; 1336 for (n = 0; folders[n]; n++) { 1337 msg = xs_dict_set_fmt(msg, folders[n], "%s/%s", snac->actor, folders[n]); 1338 } 1339 1340 p = xs_dict_get(snac->config, "avatar"); 1341 1342 if (*p == '\0') 1343 p = avtr = xs_fmt("%s/susie.png", srv_baseurl); 1344 1345 icon = xs_dict_append(icon, "type", "Image"); 1346 icon = xs_dict_append(icon, "mediaType", xs_mime_by_ext(p)); 1347 icon = xs_dict_append(icon, "url", p); 1348 msg = xs_dict_set(msg, "icon", icon); 1349 1350 kid = xs_fmt("%s#main-key", snac->actor); 1351 1352 keys = xs_dict_append(keys, "id", kid); 1353 keys = xs_dict_append(keys, "owner", snac->actor); 1354 keys = xs_dict_append(keys, "publicKeyPem", xs_dict_get(snac->key, "public")); 1355 msg = xs_dict_set(msg, "publicKey", keys); 1356 1357 /* if the "bot" config field is set to true, change type to "Service" */ 1358 if (xs_type(xs_dict_get(snac->config, "bot")) == XSTYPE_TRUE) 1359 msg = xs_dict_set(msg, "type", "Service"); 1360 1361 /* if it's named "relay", then identify as an "Application" */ 1362 if (strcmp(snac->uid, "relay") == 0) 1363 msg = xs_dict_set(msg, "type", "Application"); 1364 1365 /* add the header image, if there is one defined */ 1366 const char *header = xs_dict_get(snac->config, "header"); 1367 if (!xs_is_null(header)) { 1368 xs *d = xs_dict_new(); 1369 d = xs_dict_append(d, "type", "Image"); 1370 d = xs_dict_append(d, "mediaType", xs_mime_by_ext(header)); 1371 d = xs_dict_append(d, "url", header); 1372 msg = xs_dict_set(msg, "image", d); 1373 } 1374 1375 /* add the metadata as attachments of PropertyValue */ 1376 xs *metadata = NULL; 1377 const xs_dict *md = xs_dict_get(snac->config, "metadata"); 1378 1379 if (xs_type(md) == XSTYPE_DICT) 1380 metadata = xs_dup(md); 1381 else 1382 if (xs_type(md) == XSTYPE_STRING) { 1383 metadata = xs_dict_new(); 1384 xs *l = xs_split(md, "\n"); 1385 const char *ll; 1386 1387 xs_list_foreach(l, ll) { 1388 xs *kv = xs_split_n(ll, "=", 1); 1389 const char *k = xs_list_get(kv, 0); 1390 const char *v = xs_list_get(kv, 1); 1391 1392 if (k && v) { 1393 xs *kk = xs_strip_i(xs_dup(k)); 1394 xs *vv = xs_strip_i(xs_dup(v)); 1395 metadata = xs_dict_set(metadata, kk, vv); 1396 } 1397 } 1398 } 1399 1400 if (xs_type(metadata) == XSTYPE_DICT) { 1401 xs *attach = xs_list_new(); 1402 const xs_str *k; 1403 const xs_str *v; 1404 1405 int c = 0; 1406 while (xs_dict_next(metadata, &k, &v, &c)) { 1407 xs *d = xs_dict_new(); 1408 1409 xs *k2 = encode_html(k); 1410 xs *v2 = NULL; 1411 1412 if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/")) { 1413 xs *t = encode_html(v); 1414 v2 = xs_fmt("<a href=\"%s\" rel=\"me\">%s</a>", t, t); 1415 } 1416 else 1417 v2 = encode_html(v); 1418 1419 d = xs_dict_append(d, "type", "PropertyValue"); 1420 d = xs_dict_append(d, "name", k2); 1421 d = xs_dict_append(d, "value", v2); 1422 1423 attach = xs_list_append(attach, d); 1424 } 1425 1426 msg = xs_dict_set(msg, "attachment", attach); 1427 } 1428 1429 /* use shared inboxes? */ 1430 if (xs_is_true(xs_dict_get(srv_config, "shared_inboxes")) || strcmp(snac->uid, "relay") == 0) { 1431 xs *d = xs_dict_new(); 1432 xs *si = xs_fmt("%s/shared-inbox", srv_baseurl); 1433 d = xs_dict_append(d, "sharedInbox", si); 1434 msg = xs_dict_set(msg, "endpoints", d); 1435 } 1436 1437 /* does this user have an aka? */ 1438 const char *aka = xs_dict_get(snac->config, "alias"); 1439 if (xs_type(aka) == XSTYPE_STRING && *aka) { 1440 xs *loaka = xs_list_append(xs_list_new(), aka); 1441 1442 msg = xs_dict_set(msg, "alsoKnownAs", loaka); 1443 } 1444 1445 const xs_val *manually = xs_dict_get(snac->config, "approve_followers"); 1446 msg = xs_dict_set(msg, "manuallyApprovesFollowers", 1447 xs_stock(xs_is_true(manually) ? XSTYPE_TRUE : XSTYPE_FALSE)); 1448 1449 /* if there are location coords, create a Place object */ 1450 xs *location = msg_actor_place(snac, "Home"); 1451 if (xs_type(location) == XSTYPE_DICT) 1452 msg = xs_dict_set(msg, "location", location); 1453 1454 /* cache it */ 1455 snac_debug(snac, 1, xs_fmt("Caching actor %s", snac->actor)); 1456 object_add_ow(snac->actor, msg); 1457 1458 return msg; 1459 } 1460 1461 1462 xs_dict *msg_create(snac *snac, const xs_dict *object) 1463 /* creates a 'Create' message */ 1464 { 1465 xs_dict *msg = msg_base(snac, "Create", "@wrapper", snac->actor, NULL, object); 1466 const xs_val *v; 1467 1468 if ((v = get_atto(object))) 1469 msg = xs_dict_append(msg, "attributedTo", v); 1470 1471 if ((v = xs_dict_get(object, "cc"))) 1472 msg = xs_dict_append(msg, "cc", v); 1473 1474 if ((v = xs_dict_get(object, "to"))) 1475 msg = xs_dict_append(msg, "to", v); 1476 else 1477 msg = xs_dict_append(msg, "to", public_address); 1478 1479 return msg; 1480 } 1481 1482 1483 xs_dict *msg_undo(snac *snac, const xs_val *object) 1484 /* creates an 'Undo' message */ 1485 { 1486 xs_dict *msg = msg_base(snac, "Undo", "@object", snac->actor, "@now", object); 1487 const char *to; 1488 1489 if (xs_type(object) == XSTYPE_DICT && (to = xs_dict_get(object, "object"))) 1490 msg = xs_dict_append(msg, "to", to); 1491 1492 return msg; 1493 } 1494 1495 1496 xs_dict *msg_delete(snac *snac, const char *id) 1497 /* creates a 'Delete' + 'Tombstone' for a local entry */ 1498 { 1499 xs *tomb = xs_dict_new(); 1500 xs_dict *msg = NULL; 1501 1502 /* sculpt the tombstone */ 1503 tomb = xs_dict_append(tomb, "type", "Tombstone"); 1504 tomb = xs_dict_append(tomb, "id", id); 1505 1506 /* now create the Delete */ 1507 msg = msg_base(snac, "Delete", "@object", snac->actor, "@now", tomb); 1508 1509 msg = xs_dict_append(msg, "to", public_address); 1510 1511 return msg; 1512 } 1513 1514 1515 xs_dict *msg_follow(snac *snac, const char *q) 1516 /* creates a 'Follow' message */ 1517 { 1518 xs *actor_o = NULL; 1519 xs *actor = NULL; 1520 xs_dict *msg = NULL; 1521 int status; 1522 1523 xs *url_or_uid = xs_strip_i(xs_str_new(q)); 1524 1525 if (xs_startswith(url_or_uid, "https:/") || xs_startswith(url_or_uid, "http:/")) 1526 actor = xs_dup(url_or_uid); 1527 else 1528 if (!valid_status(webfinger_request(url_or_uid, &actor, NULL)) || actor == NULL) { 1529 snac_log(snac, xs_fmt("cannot resolve user %s to follow", url_or_uid)); 1530 return NULL; 1531 } 1532 1533 /* request the actor */ 1534 status = actor_request(snac, actor, &actor_o); 1535 1536 if (valid_status(status)) { 1537 /* check if the actor is an alias */ 1538 const char *r_actor = xs_dict_get(actor_o, "id"); 1539 1540 if (r_actor && strcmp(actor, r_actor) != 0) { 1541 snac_log(snac, xs_fmt("actor to follow is an alias %s -> %s", actor, r_actor)); 1542 } 1543 1544 msg = msg_base(snac, "Follow", "@dummy", snac->actor, NULL, r_actor); 1545 } 1546 else 1547 snac_log(snac, xs_fmt("cannot get actor to follow %s %d", actor, status)); 1548 1549 return msg; 1550 } 1551 1552 1553 xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, 1554 const xs_str *in_reply_to, const xs_list *attach, 1555 int scope, const char *lang_str, const char *msg_date) 1556 /* creates a 'Note' message */ 1557 /* scope: 0, public; 1, private (mentioned only); 2, "quiet public"; 3, followers only */ 1558 { 1559 xs *ntid = tid(0); 1560 xs *id = xs_fmt("%s/p/%s", snac->actor, ntid); 1561 xs *ctxt = NULL; 1562 xs *fc2 = NULL; 1563 xs *fc1 = NULL; 1564 xs *to = NULL; 1565 xs *cc = xs_list_new(); 1566 xs *irt = NULL; 1567 xs *tag = xs_list_new(); 1568 xs *atls = xs_list_new(); 1569 1570 /* discard non-parseable dates */ 1571 if (!xs_is_string(msg_date) || xs_parse_iso_date(msg_date, 0) == 0) 1572 msg_date = NULL; 1573 1574 xs_dict *msg = msg_base(snac, "Note", id, NULL, xs_or(msg_date, "@now"), NULL); 1575 xs_list *p; 1576 const xs_val *v; 1577 1578 /* FIXME: implement scope 3 */ 1579 int priv = scope == 1; 1580 1581 if (rcpts == NULL) 1582 to = xs_list_new(); 1583 else { 1584 if (xs_type(rcpts) == XSTYPE_STRING) { 1585 to = xs_list_new(); 1586 to = xs_list_append(to, rcpts); 1587 } 1588 else 1589 to = xs_dup(rcpts); 1590 } 1591 1592 /* format the content */ 1593 fc2 = not_really_markdown(content, &atls, &tag); 1594 1595 if (in_reply_to != NULL && *in_reply_to) { 1596 xs *p_msg = NULL; 1597 xs *wrk = NULL; 1598 1599 /* demand this thing */ 1600 timeline_request(snac, &in_reply_to, &wrk, 0); 1601 1602 if (valid_status(object_get(in_reply_to, &p_msg))) { 1603 /* add this author as recipient */ 1604 const char *a, *v; 1605 1606 if ((a = get_atto(p_msg)) && xs_list_in(to, a) == -1) 1607 to = xs_list_append(to, a); 1608 1609 /* add this author to the tag list as a mention */ 1610 if (!xs_is_null(a)) { 1611 xs *l = xs_split(a, "/"); 1612 xs *actor_o = NULL; 1613 1614 if (xs_list_len(l) > 3 && valid_status(object_get(a, &actor_o))) { 1615 const char *uname = xs_dict_get(actor_o, "preferredUsername"); 1616 1617 if (!xs_is_null(uname) && *uname) { 1618 xs *handle = xs_fmt("@%s@%s", uname, xs_list_get(l, 2)); 1619 1620 xs *t = xs_dict_new(); 1621 1622 t = xs_dict_append(t, "type", "Mention"); 1623 t = xs_dict_append(t, "href", a); 1624 t = xs_dict_append(t, "name", handle); 1625 1626 tag = xs_list_append(tag, t); 1627 } 1628 } 1629 } 1630 1631 /* get the context, if there is one */ 1632 if ((v = xs_dict_get(p_msg, "context"))) 1633 ctxt = xs_dup(v); 1634 1635 /* propagate the conversation field, if there is one */ 1636 if ((v = xs_dict_get(p_msg, "conversation"))) 1637 msg = xs_dict_append(msg, "conversation", v); 1638 1639 /* if this message is public, ours will also be */ 1640 if (!priv && is_msg_public(p_msg) && xs_list_in(to, public_address) == -1) 1641 to = xs_list_append(to, public_address); 1642 } 1643 1644 irt = xs_dup(in_reply_to); 1645 } 1646 else 1647 irt = xs_val_new(XSTYPE_NULL); 1648 1649 /* extract the mentions and hashtags and convert the content */ 1650 fc1 = process_tags(snac, fc2, &tag); 1651 1652 /* create the attachment list, if there are any */ 1653 if (!xs_is_null(attach)) { 1654 xs_list_foreach(attach, v) { 1655 const char *url = xs_list_get(v, 0); 1656 const char *alt = xs_list_get(v, 1); 1657 const char *mime = xs_mime_by_ext(url); 1658 int add = 1; 1659 1660 /* check if it's already here */ 1661 const xs_dict *ad; 1662 xs_list_foreach(atls, ad) { 1663 if (strcmp(xs_dict_get_def(ad, "url", ""), url) == 0) { 1664 add = 0; 1665 break; 1666 } 1667 } 1668 1669 if (add) { 1670 xs *d = xs_dict_new(); 1671 d = xs_dict_append(d, "mediaType", mime); 1672 d = xs_dict_append(d, "url", url); 1673 d = xs_dict_append(d, "name", alt); 1674 d = xs_dict_append(d, "type", 1675 xs_startswith(mime, "image/") ? "Image" : "Document"); 1676 1677 atls = xs_list_append(atls, d); 1678 } 1679 } 1680 } 1681 1682 if (ctxt == NULL) 1683 ctxt = xs_fmt("%s#ctxt", id); 1684 1685 /* add all mentions to the cc */ 1686 p = tag; 1687 while (xs_list_iter(&p, &v)) { 1688 if (xs_type(v) == XSTYPE_DICT) { 1689 const char *t; 1690 1691 if (!xs_is_null(t = xs_dict_get(v, "type")) && strcmp(t, "Mention") == 0) { 1692 if (!xs_is_null(t = xs_dict_get(v, "href"))) 1693 cc = xs_list_append(cc, t); 1694 } 1695 } 1696 } 1697 1698 if (scope == 2) { 1699 /* Mastodon's "quiet public": add public address to cc */ 1700 if (xs_list_in(cc, public_address) == -1) 1701 cc = xs_list_append(cc, public_address); 1702 } 1703 else 1704 /* no recipients? must be for everybody */ 1705 if (!priv && xs_list_len(to) == 0) 1706 to = xs_list_append(to, public_address); 1707 1708 /* delete all cc recipients that also are in the to */ 1709 p = to; 1710 while (xs_list_iter(&p, &v)) { 1711 int i; 1712 1713 if ((i = xs_list_in(cc, v)) != -1) 1714 cc = xs_list_del(cc, i); 1715 } 1716 1717 msg = xs_dict_append(msg, "attributedTo", snac->actor); 1718 msg = xs_dict_append(msg, "summary", ""); 1719 msg = xs_dict_append(msg, "content", fc1); 1720 msg = xs_dict_append(msg, "context", ctxt); 1721 msg = xs_dict_append(msg, "url", id); 1722 msg = xs_dict_append(msg, "to", to); 1723 msg = xs_dict_append(msg, "cc", cc); 1724 msg = xs_dict_append(msg, "inReplyTo", irt); 1725 msg = xs_dict_append(msg, "tag", tag); 1726 1727 msg = xs_dict_append(msg, "sourceContent", content); 1728 1729 if (xs_list_len(atls)) 1730 msg = xs_dict_append(msg, "attachment", atls); 1731 1732 /* set language content map */ 1733 if (xs_type(lang_str) == XSTYPE_STRING) { 1734 /* split at the first _ */ 1735 xs *l0 = xs_split(lang_str, "_"); 1736 const char *lang = xs_list_get(l0, 0); 1737 1738 if (xs_type(lang) == XSTYPE_STRING && strlen(lang) == 2) { 1739 /* a valid ISO language id */ 1740 xs *cmap = xs_dict_new(); 1741 cmap = xs_dict_set(cmap, lang, xs_dict_get(msg, "content")); 1742 msg = xs_dict_set(msg, "contentMap", cmap); 1743 } 1744 } 1745 1746 return msg; 1747 } 1748 1749 1750 xs_dict *msg_ping(snac *user, const char *rcpt) 1751 /* creates a Ping message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */ 1752 { 1753 xs_dict *msg = msg_base(user, "Ping", "@dummy", user->actor, NULL, NULL); 1754 1755 msg = xs_dict_append(msg, "to", rcpt); 1756 1757 return msg; 1758 } 1759 1760 1761 xs_dict *msg_pong(snac *user, const char *rcpt, const char *object) 1762 /* creates a Pong message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */ 1763 { 1764 xs_dict *msg = msg_base(user, "Pong", "@dummy", user->actor, NULL, object); 1765 1766 msg = xs_dict_append(msg, "to", rcpt); 1767 1768 return msg; 1769 } 1770 1771 1772 xs_dict *msg_move(snac *user, const char *new_account) 1773 /* creates a Move message (to move the user to new_account) */ 1774 { 1775 xs_dict *msg = msg_base(user, "Move", "@dummy", user->actor, NULL, user->actor); 1776 1777 msg = xs_dict_append(msg, "target", new_account); 1778 1779 return msg; 1780 } 1781 1782 1783 xs_dict *msg_question(snac *user, const char *content, xs_list *attach, 1784 const xs_list *opts, int multiple, int end_secs) 1785 /* creates a Question message */ 1786 { 1787 xs_dict *msg = msg_note(user, content, NULL, NULL, attach, 0, NULL, NULL); 1788 int max = 8; 1789 xs_set seen; 1790 1791 msg = xs_dict_set(msg, "type", "Question"); 1792 1793 /* make it non-editable */ 1794 msg = xs_dict_del(msg, "sourceContent"); 1795 1796 xs *o = xs_list_new(); 1797 xs_list *p = (xs_list *)opts; 1798 const xs_str *v; 1799 xs_val *replies = NULL; 1800 1801 xs_set_init(&seen); 1802 1803 while (max && xs_list_iter(&p, &v)) { 1804 if (*v) { 1805 if (replies == NULL) 1806 replies = xs_json_loads("{\"type\":\"Collection\",\"totalItems\":0}"); 1807 const xs_val *vv = v; 1808 xs *v2 = NULL; 1809 1810 if (strlen(v) > 62) { 1811 vv = v2 = xs_realloc(NULL, 64); 1812 memcpy(v2, v, 60); 1813 strcpy(v2 + 60, "..."); 1814 } 1815 1816 if (xs_set_add(&seen, v2) == 1) { 1817 xs *d = xs_dict_new(); 1818 d = xs_dict_append(d, "type", "Note"); 1819 d = xs_dict_append(d, "name", vv); 1820 d = xs_dict_append(d, "replies", replies); 1821 o = xs_list_append(o, d); 1822 1823 max--; 1824 } 1825 } 1826 } 1827 1828 xs_set_free(&seen); 1829 1830 msg = xs_dict_append(msg, multiple ? "anyOf" : "oneOf", o); 1831 1832 /* set the end time */ 1833 time_t t = time(NULL) + end_secs; 1834 xs *et = xs_str_utctime(t, ISO_DATE_SPEC); 1835 1836 msg = xs_dict_append(msg, "endTime", et); 1837 1838 return msg; 1839 } 1840 1841 1842 int update_question(snac *user, const char *id) 1843 /* updates the poll counts */ 1844 { 1845 xs *msg = NULL; 1846 xs *rcnt = xs_dict_new(); 1847 xs *lopts = xs_list_new(); 1848 const xs_list *opts; 1849 xs_list *p; 1850 const xs_val *v; 1851 1852 /* get the object */ 1853 if (!valid_status(object_get(id, &msg))) 1854 return -1; 1855 1856 /* closed? do nothing more */ 1857 if (xs_dict_get(msg, "closed")) 1858 return -2; 1859 1860 /* get the options */ 1861 if ((opts = xs_dict_get(msg, "oneOf")) == NULL && 1862 (opts = xs_dict_get(msg, "anyOf")) == NULL) 1863 return -3; 1864 1865 /* fill the initial count */ 1866 int c = 0; 1867 while (xs_list_next(opts, &v, &c)) { 1868 const char *name = xs_dict_get(v, "name"); 1869 if (name) { 1870 lopts = xs_list_append(lopts, name); 1871 rcnt = xs_dict_set(rcnt, name, xs_stock(0)); 1872 } 1873 } 1874 1875 xs_set s; 1876 xs_set_init(&s); 1877 1878 /* iterate now the children (the votes) */ 1879 xs *chld = object_children(id); 1880 p = chld; 1881 while (xs_list_iter(&p, &v)) { 1882 xs *obj = NULL; 1883 1884 if (!valid_status(object_get_by_md5(v, &obj))) 1885 continue; 1886 1887 const char *name = xs_dict_get(obj, "name"); 1888 const char *atto = get_atto(obj); 1889 1890 if (name && atto) { 1891 /* get the current count */ 1892 const xs_number *cnt = xs_dict_get(rcnt, name); 1893 1894 if (xs_type(cnt) == XSTYPE_NUMBER) { 1895 /* if it exists, increment */ 1896 xs *ucnt = xs_number_new(xs_number_get(cnt) + 1); 1897 rcnt = xs_dict_set(rcnt, name, ucnt); 1898 1899 xs_set_add(&s, atto); 1900 } 1901 } 1902 } 1903 1904 xs *rcpts = xs_set_result(&s); 1905 1906 /* create a new list of options with their new counts */ 1907 xs *nopts = xs_list_new(); 1908 p = lopts; 1909 while (xs_list_iter(&p, &v)) { 1910 const xs_number *cnt = xs_dict_get(rcnt, v); 1911 1912 if (xs_type(cnt) == XSTYPE_NUMBER) { 1913 xs *d1 = xs_dict_new(); 1914 xs *d2 = xs_dict_new(); 1915 1916 d2 = xs_dict_append(d2, "type", "Collection"); 1917 d2 = xs_dict_append(d2, "totalItems", cnt); 1918 1919 d1 = xs_dict_append(d1, "type", "Note"); 1920 d1 = xs_dict_append(d1, "name", v); 1921 d1 = xs_dict_append(d1, "replies", d2); 1922 1923 nopts = xs_list_append(nopts, d1); 1924 } 1925 } 1926 1927 /* update the list */ 1928 msg = xs_dict_set(msg, xs_dict_get(msg, "oneOf") != NULL ? "oneOf" : "anyOf", nopts); 1929 1930 /* due date? */ 1931 int closed = 0; 1932 const char *end_time = xs_dict_get(msg, "endTime"); 1933 if (!xs_is_null(end_time)) { 1934 xs *now = xs_str_utctime(0, ISO_DATE_SPEC); 1935 1936 /* is now greater than the endTime? */ 1937 if (strcmp(now, end_time) >= 0) { 1938 xs *et = xs_dup(end_time); 1939 msg = xs_dict_set(msg, "closed", et); 1940 1941 closed = 1; 1942 } 1943 } 1944 1945 /* update the count of voters */ 1946 xs *vcnt = xs_number_new(xs_list_len(rcpts)); 1947 msg = xs_dict_set(msg, "votersCount", vcnt); 1948 msg = xs_dict_set(msg, "cc", rcpts); 1949 1950 /* store */ 1951 object_add_ow(id, msg); 1952 1953 snac_debug(user, 1, xs_fmt("recounted poll %s", id)); 1954 timeline_touch(user); 1955 1956 /* send an update message to all voters */ 1957 xs *u_msg = msg_update(user, msg); 1958 u_msg = xs_dict_set(u_msg, "cc", rcpts); 1959 1960 enqueue_message(user, u_msg); 1961 1962 if (closed) { 1963 xs *c_msg = msg_update(user, msg); 1964 notify(user, "Update", "Question", user->actor, c_msg); 1965 } 1966 1967 return 0; 1968 } 1969 1970 1971 /** queues **/ 1972 1973 int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) 1974 /* processes an ActivityPub message from the input queue */ 1975 /* return values: -1, fatal error; 0, transient error, retry; 1976 1, processed and done; 2, propagate to users (only when no user is set) */ 1977 { 1978 const char *actor = xs_dict_get(msg, "actor"); 1979 const char *type = xs_dict_get(msg, "type"); 1980 xs *actor_o = NULL; 1981 int a_status; 1982 int do_notify = 0; 1983 1984 if (xs_is_null(actor) || *actor == '\0') { 1985 srv_debug(0, xs_fmt("malformed message (bad actor)")); 1986 return -1; 1987 } 1988 1989 /* question votes may not have a type */ 1990 if (xs_is_null(type)) 1991 type = "Note"; 1992 1993 /* reject uninteresting messages right now */ 1994 if (xs_match(type, "Add|View|Reject|Read|Remove")) { 1995 srv_debug(0, xs_fmt("Ignored message of type '%s'", type)); 1996 1997 /* archive the ignored activity */ 1998 xs *ntid = tid(0); 1999 xs *fn = xs_fmt("%s/ignored/%s.json", srv_basedir, ntid); 2000 FILE *f; 2001 2002 if ((f = fopen(fn, "w")) != NULL) { 2003 xs_json_dump(msg, 4, f); 2004 fclose(f); 2005 } 2006 2007 return -1; 2008 } 2009 2010 const char *object, *utype; 2011 2012 object = xs_dict_get(msg, "object"); 2013 if (object != NULL && xs_type(object) == XSTYPE_DICT) 2014 utype = xs_dict_get(object, "type"); 2015 else 2016 utype = "(null)"; 2017 2018 /* special case for Delete messages */ 2019 if (strcmp(type, "Delete") == 0) { 2020 /* if the actor is not here, do not even try */ 2021 if (!object_here(actor)) { 2022 srv_debug(1, xs_fmt("dropped 'Delete' message from unknown actor '%s'", actor)); 2023 return -1; 2024 } 2025 2026 /* discard crap */ 2027 if (xs_is_null(object)) { 2028 srv_log(xs_fmt("dropped 'Delete' message with invalid object from actor '%s'", actor)); 2029 return -1; 2030 } 2031 2032 /* also discard if the object to be deleted is not here */ 2033 const char *obj_id = object; 2034 if (xs_type(obj_id) == XSTYPE_DICT) 2035 obj_id = xs_dict_get(obj_id, "id"); 2036 2037 if (xs_is_null(obj_id) || !object_here(obj_id)) { 2038 srv_debug(1, xs_fmt("dropped 'Delete' message from unknown object '%s'", obj_id)); 2039 return -1; 2040 } 2041 } 2042 2043 /* bring the actor */ 2044 a_status = actor_request(snac, actor, &actor_o); 2045 2046 /* do not retry permanent failures */ 2047 if (a_status == HTTP_STATUS_NOT_FOUND 2048 || a_status == HTTP_STATUS_GONE 2049 || a_status < 0) { 2050 srv_debug(1, xs_fmt("dropping message due to actor error %s %d", actor, a_status)); 2051 return -1; 2052 } 2053 2054 if (!valid_status(a_status)) { 2055 /* do not retry 'Delete' messages */ 2056 if (strcmp(type, "Delete") == 0) { 2057 srv_debug(1, xs_fmt("dropping 'Delete' message due to actor error %s %d", actor, a_status)); 2058 return -1; 2059 } 2060 2061 /* other actor download errors */ 2062 2063 /* the actor may require a signed request; propagate if no user is set */ 2064 if (snac == NULL) 2065 return 2; 2066 2067 /* may need a retry */ 2068 srv_debug(0, xs_fmt("error requesting actor %s %d -- retry later", actor, a_status)); 2069 return 0; 2070 } 2071 2072 /* check the signature */ 2073 xs *sig_err = NULL; 2074 2075 if (!check_signature(req, &sig_err)) { 2076 srv_log(xs_fmt("bad signature %s (%s)", actor, sig_err)); 2077 2078 srv_archive_error("check_signature", sig_err, req, msg); 2079 return -1; 2080 } 2081 2082 /* if no user is set, no further checks can be done; propagate */ 2083 if (snac == NULL) 2084 return 2; 2085 2086 /* reject messages that are not for this user */ 2087 if (!is_msg_for_me(snac, msg)) { 2088 snac_debug(snac, 1, xs_fmt("message from %s of type '%s' not for us", actor, type)); 2089 2090 return 1; 2091 } 2092 2093 /* if it's a DM from someone we don't follow, reject the message */ 2094 if (xs_type(xs_dict_get(snac->config, "drop_dm_from_unknown")) == XSTYPE_TRUE) { 2095 if (strcmp(utype, "Note") == 0 && !is_msg_public(msg) && 2096 !following_check(snac, actor)) { 2097 snac_log(snac, xs_fmt("DM rejected from unknown actor %s", actor)); 2098 2099 return 1; 2100 } 2101 } 2102 2103 /* check the minimum acceptable account age */ 2104 int min_account_age = xs_number_get(xs_dict_get(srv_config, "min_account_age")); 2105 2106 if (min_account_age > 0) { 2107 const char *actor_date = xs_dict_get(actor_o, "published"); 2108 if (!xs_is_null(actor_date)) { 2109 time_t actor_t = xs_parse_iso_date(actor_date, 0); 2110 2111 if (actor_t < 950000000) { 2112 snac_log(snac, xs_fmt("rejected activity from %s (suspicious date, %s)", 2113 actor, actor_date)); 2114 2115 return 1; 2116 } 2117 2118 if (actor_t > 0) { 2119 int td = (int)(time(NULL) - actor_t); 2120 2121 snac_debug(snac, 2, xs_fmt("actor %s age: %d seconds", actor, td)); 2122 2123 if (td < min_account_age) { 2124 snac_log(snac, xs_fmt("rejected activity from %s (too new, %d seconds)", 2125 actor, td)); 2126 2127 return 1; 2128 } 2129 } 2130 } 2131 else 2132 snac_debug(snac, 1, xs_fmt("warning: empty or null creation date for %s", actor)); 2133 } 2134 2135 if (strcmp(type, "Follow") == 0) { /** **/ 2136 const char *id = xs_dict_get(msg, "id"); 2137 2138 if (xs_is_null(id)) { 2139 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2140 } 2141 else 2142 if (!follower_check(snac, actor)) { 2143 /* ensure the actor object is here */ 2144 if (!object_here(actor)) { 2145 xs *actor_obj = NULL; 2146 actor_request(snac, actor, &actor_obj); 2147 object_add(actor, actor_obj); 2148 } 2149 2150 if (xs_is_true(xs_dict_get(snac->config, "approve_followers"))) { 2151 pending_add(snac, actor, msg); 2152 2153 snac_log(snac, xs_fmt("new pending follower approval %s", actor)); 2154 } 2155 else { 2156 /* automatic following */ 2157 xs *f_msg = xs_dup(msg); 2158 xs *reply = msg_accept(snac, f_msg, actor); 2159 2160 post_message(snac, actor, reply); 2161 2162 if (xs_is_null(xs_dict_get(f_msg, "published"))) { 2163 /* add a date if it doesn't include one (Mastodon) */ 2164 xs *date = xs_str_utctime(0, ISO_DATE_SPEC); 2165 f_msg = xs_dict_set(f_msg, "published", date); 2166 } 2167 2168 timeline_add(snac, id, f_msg); 2169 2170 follower_add(snac, actor); 2171 2172 snac_log(snac, xs_fmt("new follower %s", actor)); 2173 } 2174 2175 do_notify = 1; 2176 } 2177 else 2178 snac_log(snac, xs_fmt("repeated 'Follow' from %s", actor)); 2179 } 2180 else 2181 if (strcmp(type, "Undo") == 0) { /** **/ 2182 const char *id = xs_dict_get(object, "object"); 2183 2184 if (xs_type(object) != XSTYPE_DICT) { 2185 snac_debug(snac, 1, xs_fmt("undo: overriding utype %s | %s | %s", 2186 utype, id, actor)); 2187 utype = "Follow"; 2188 } 2189 2190 if (strcmp(utype, "Follow") == 0) { /** **/ 2191 if (!id) { 2192 snac_log(snac, xs_fmt("no id (msg.object.object) when " 2193 "handling follow: %s", actor)); 2194 return 1; 2195 } 2196 if (strcmp(id, snac->actor) != 0) 2197 snac_debug(snac, 1, xs_fmt("Undo + Follow from %s not for us (%s)", actor, id)); 2198 else 2199 if (valid_status(follower_del(snac, actor))) { 2200 snac_log(snac, xs_fmt("no longer following us %s", actor)); 2201 do_notify = 1; 2202 } 2203 else 2204 if (pending_check(snac, actor)) { 2205 pending_del(snac, actor); 2206 snac_log(snac, xs_fmt("cancelled pending follow from %s", actor)); 2207 } 2208 else 2209 snac_log(snac, xs_fmt("error deleting follower %s", actor)); 2210 } 2211 else 2212 if (strcmp(utype, "Like") == 0 || strcmp(utype, "EmojiReact") == 0) { /** **/ 2213 int status = object_unadmire(id, actor, 1); 2214 2215 snac_log(snac, xs_fmt("Undo '%s' for %s %d", utype, id, status)); 2216 } 2217 else 2218 if (strcmp(utype, "Announce") == 0) { /** **/ 2219 int status = HTTP_STATUS_OK; 2220 2221 /* commented out: if a followed user boosts something that 2222 is requested and then unboosts, the post remains here, 2223 but with no apparent reason, and that is confusing */ 2224 //status = object_unadmire(id, actor, 0); 2225 2226 snac_log(snac, xs_fmt("Unboost for %s %d", id, status)); 2227 } 2228 else 2229 snac_debug(snac, 1, xs_fmt("ignored 'Undo' for object type '%s'", utype)); 2230 } 2231 else 2232 if (strcmp(type, "Create") == 0) { /** **/ 2233 if (is_muted(snac, actor)) { 2234 snac_log(snac, xs_fmt("ignored 'Create' + '%s' from muted actor %s", utype, actor)); 2235 return 1; 2236 } 2237 2238 if (xs_match(utype, "Note|Article")) { /** **/ 2239 const char *id = xs_dict_get(object, "id"); 2240 const char *in_reply_to = get_in_reply_to(object); 2241 const char *atto = get_atto(object); 2242 xs *wrk = NULL; 2243 2244 if (xs_is_null(id)) 2245 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2246 else 2247 if (xs_is_null(atto)) 2248 snac_log(snac, xs_fmt("malformed message: no 'attributedTo' field")); 2249 else 2250 if (!xs_is_null(in_reply_to) && is_hidden(snac, in_reply_to)) { 2251 snac_debug(snac, 0, xs_fmt("dropped reply %s to hidden post %s", id, in_reply_to)); 2252 } 2253 else { 2254 if (content_match("filter_reject.txt", object)) { 2255 snac_log(snac, xs_fmt("rejected by content %s", id)); 2256 return 1; 2257 } 2258 2259 if (strcmp(actor, atto) != 0) 2260 snac_log(snac, xs_fmt("SUSPICIOUS: actor != atto (%s != %s)", actor, atto)); 2261 2262 timeline_request(snac, &in_reply_to, &wrk, 0); 2263 2264 if (timeline_add(snac, id, object)) { 2265 snac_log(snac, xs_fmt("new '%s' %s %s", utype, actor, id)); 2266 do_notify = 1; 2267 } 2268 2269 /* if it has a "name" field, it may be a vote for a question */ 2270 const char *name = xs_dict_get(object, "name"); 2271 2272 if (!xs_is_null(name) && *name && !xs_is_null(in_reply_to) && *in_reply_to) 2273 update_question(snac, in_reply_to); 2274 } 2275 } 2276 else 2277 if (strcmp(utype, "Question") == 0) { /** **/ 2278 const char *id = xs_dict_get(object, "id"); 2279 2280 if (xs_is_null(id)) 2281 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2282 else 2283 if (timeline_add(snac, id, object)) 2284 snac_log(snac, xs_fmt("new 'Question' %s %s", actor, id)); 2285 } 2286 else 2287 if (xs_match(utype, "Audio|Video|Event")) { /** **/ 2288 const char *id = xs_dict_get(object, "id"); 2289 2290 if (xs_is_null(id)) 2291 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2292 else 2293 if (timeline_add(snac, id, object)) 2294 snac_log(snac, xs_fmt("new '%s' %s %s", utype, actor, id)); 2295 } 2296 else 2297 snac_debug(snac, 1, xs_fmt("ignored 'Create' for object type '%s'", utype)); 2298 } 2299 else 2300 if (strcmp(type, "Accept") == 0) { /** **/ 2301 if (strcmp(utype, "(null)") == 0) { 2302 const char *obj_id = xs_dict_get(msg, "object"); 2303 2304 /* if the accepted object id is a string that may 2305 be created by us, it's a follow */ 2306 if (xs_type(obj_id) == XSTYPE_STRING && 2307 xs_startswith(obj_id, srv_baseurl) && 2308 xs_endswith(obj_id, "/Follow")) 2309 utype = "Follow"; 2310 } 2311 2312 if (strcmp(utype, "Follow") == 0) { /** **/ 2313 if (following_check(snac, actor)) { 2314 following_add(snac, actor, msg); 2315 snac_log(snac, xs_fmt("confirmed follow from %s", actor)); 2316 } 2317 else 2318 snac_log(snac, xs_fmt("spurious follow accept from %s", actor)); 2319 } 2320 else 2321 if (strcmp(utype, "Create") == 0) { 2322 /* some implementations send Create confirmations, go figure */ 2323 snac_debug(snac, 1, xs_dup("ignored 'Accept' + 'Create'")); 2324 } 2325 else { 2326 srv_archive_error("accept", "ignored Accept", req, msg); 2327 snac_debug(snac, 1, xs_fmt("ignored 'Accept' for object type '%s'", utype)); 2328 } 2329 } 2330 else 2331 if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0) { /** **/ 2332 if (xs_type(object) == XSTYPE_DICT) 2333 object = xs_dict_get(object, "id"); 2334 2335 if (xs_is_null(object)) 2336 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2337 else 2338 if (timeline_admire(snac, object, actor, 1) == HTTP_STATUS_CREATED) 2339 snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object)); 2340 else 2341 snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object)); 2342 2343 do_notify = 1; 2344 } 2345 else 2346 if (strcmp(type, "Announce") == 0) { /** **/ 2347 if (xs_type(object) == XSTYPE_DICT) 2348 object = xs_dict_get(object, "id"); 2349 2350 if (xs_is_null(object)) 2351 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2352 else 2353 if (is_muted(snac, actor) && !xs_startswith(object, snac->actor)) 2354 snac_log(snac, xs_fmt("dropped 'Announce' from muted actor %s", actor)); 2355 else 2356 if (is_limited(snac, actor) && !xs_startswith(object, snac->actor)) 2357 snac_log(snac, xs_fmt("dropped 'Announce' from limited actor %s", actor)); 2358 else { 2359 xs *a_msg = NULL; 2360 xs *wrk = NULL; 2361 2362 timeline_request(snac, &object, &wrk, 0); 2363 2364 if (valid_status(object_get(object, &a_msg))) { 2365 const char *who = get_atto(a_msg); 2366 2367 if (who && !is_muted(snac, who)) { 2368 /* bring the actor */ 2369 xs *who_o = NULL; 2370 2371 if (valid_status(actor_request(snac, who, &who_o))) { 2372 /* don't account as such announces by our own relay */ 2373 xs *this_relay = xs_fmt("%s/relay", srv_baseurl); 2374 2375 if (strcmp(actor, this_relay) != 0) { 2376 if (valid_status(timeline_admire(snac, object, actor, 0))) 2377 snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object)); 2378 else 2379 snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s", 2380 actor, object)); 2381 } 2382 2383 /* distribute the post with the actor as 'proxy' */ 2384 list_distribute(snac, actor, a_msg); 2385 2386 /* distribute the post to users following these hashtags */ 2387 followed_hashtag_distribute(a_msg); 2388 2389 do_notify = 1; 2390 } 2391 else 2392 snac_debug(snac, 1, xs_fmt("dropped 'Announce' on actor request error %s", who)); 2393 } 2394 else 2395 snac_log(snac, xs_fmt("ignored 'Announce' about muted actor %s", who)); 2396 } 2397 else 2398 snac_debug(snac, 2, xs_fmt("error requesting 'Announce' object %s", object)); 2399 } 2400 } 2401 else 2402 if (strcmp(type, "Update") == 0) { /** **/ 2403 if (xs_match(utype, "Person|Service|Application")) { /** **/ 2404 actor_add(actor, xs_dict_get(msg, "object")); 2405 timeline_touch(snac); 2406 2407 snac_log(snac, xs_fmt("updated actor %s", actor)); 2408 } 2409 else 2410 if (xs_match(utype, "Note|Page|Article|Video|Audio|Event")) { /** **/ 2411 const char *id = xs_dict_get(object, "id"); 2412 2413 if (xs_is_null(id)) 2414 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2415 else 2416 if (object_here(id)) { 2417 object_add_ow(id, object); 2418 timeline_touch(snac); 2419 2420 snac_log(snac, xs_fmt("updated '%s' %s", utype, id)); 2421 } 2422 else 2423 snac_log(snac, xs_fmt("dropped update for unknown '%s' %s", utype, id)); 2424 } 2425 else 2426 if (strcmp(utype, "Question") == 0) { /** **/ 2427 const char *id = xs_dict_get(object, "id"); 2428 const char *closed = xs_dict_get(object, "closed"); 2429 2430 if (xs_is_null(id)) 2431 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2432 else { 2433 object_add_ow(id, object); 2434 timeline_touch(snac); 2435 2436 snac_log(snac, xs_fmt("%s poll %s", closed == NULL ? "updated" : "closed", id)); 2437 2438 if (closed != NULL) 2439 do_notify = 1; 2440 } 2441 } 2442 else { 2443 srv_archive_error("unsupported_update", "unsupported_update", req, msg); 2444 2445 snac_log(snac, xs_fmt("ignored 'Update' for object type '%s'", utype)); 2446 } 2447 } 2448 else 2449 if (strcmp(type, "Delete") == 0) { /** **/ 2450 if (xs_type(object) == XSTYPE_DICT) 2451 object = xs_dict_get(object, "id"); 2452 2453 if (xs_is_null(object)) 2454 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2455 else 2456 if (object_here(object)) { 2457 timeline_del(snac, object); 2458 snac_debug(snac, 1, xs_fmt("new 'Delete' %s %s", actor, object)); 2459 } 2460 else 2461 snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object)); 2462 } 2463 else 2464 if (strcmp(type, "Pong") == 0) { /** **/ 2465 snac_log(snac, xs_fmt("'Pong' received from %s", actor)); 2466 } 2467 else 2468 if (strcmp(type, "Ping") == 0) { /** **/ 2469 const char *id = xs_dict_get(msg, "id"); 2470 2471 snac_log(snac, xs_fmt("'Ping' requested from %s", actor)); 2472 2473 if (!xs_is_null(id)) { 2474 xs *rsp = msg_pong(snac, actor, id); 2475 2476 enqueue_output_by_actor(snac, rsp, actor, 0); 2477 } 2478 } 2479 else 2480 if (strcmp(type, "Block") == 0) { /** **/ 2481 snac_debug(snac, 1, xs_fmt("'Block' received from %s", actor)); 2482 2483 /* should we MUTE the actor back? */ 2484 /* mute(snac, actor); */ 2485 2486 if (!xs_is_true(xs_dict_get(srv_config, "disable_block_notifications"))) 2487 do_notify = 1; 2488 } 2489 else 2490 if (strcmp(type, "Move") == 0) { /** **/ 2491 do_notify = 1; 2492 2493 const char *old_account = xs_dict_get(msg, "object"); 2494 const char *new_account = xs_dict_get(msg, "target"); 2495 2496 if (!xs_is_null(old_account) && !xs_is_null(new_account)) { 2497 if (following_check(snac, old_account)) { 2498 xs *n_actor = NULL; 2499 2500 if (valid_status(object_get(new_account, &n_actor))) { 2501 const xs_list *aka = xs_dict_get(n_actor, "alsoKnownAs"); 2502 2503 if (xs_type(aka) == XSTYPE_LIST) { 2504 if (xs_list_in(aka, old_account) != -1) { 2505 /* all conditions met! */ 2506 2507 /* follow new account */ 2508 xs *f_msg = msg_follow(snac, new_account); 2509 2510 if (f_msg != NULL) { 2511 const char *new_actor = xs_dict_get(f_msg, "object"); 2512 following_add(snac, new_actor, f_msg); 2513 enqueue_output_by_actor(snac, f_msg, new_actor, 0); 2514 2515 snac_log(snac, xs_fmt("'Move': following %s", new_account)); 2516 } 2517 2518 /* unfollow old account */ 2519 xs *of_msg = NULL; 2520 2521 if (valid_status(following_get(snac, old_account, &of_msg))) { 2522 xs *uf_msg = msg_undo(snac, xs_dict_get(of_msg, "object")); 2523 following_del(snac, old_account); 2524 enqueue_output_by_actor(snac, uf_msg, old_account, 0); 2525 2526 snac_log(snac, xs_fmt("'Move': unfollowing %s", old_account)); 2527 } 2528 } 2529 else 2530 snac_log(snac, xs_fmt("'Move' error: old actor %s not found in %s 'alsoKnownAs'", 2531 old_account, new_account)); 2532 } 2533 else 2534 snac_log(snac, xs_fmt("'Move' error: cannot get %s 'alsoKnownAs'", new_account)); 2535 } 2536 else { 2537 snac_log(snac, xs_fmt("'Move' error: cannot get new actor %s", new_account)); 2538 2539 /* may be a server hiccup, retry later */ 2540 return 0; 2541 } 2542 } 2543 else 2544 snac_log(snac, xs_fmt("'Move' error: actor %s is not being followed", old_account)); 2545 } 2546 else { 2547 snac_log(snac, xs_fmt("'Move' error: malformed message from %s", actor)); 2548 srv_archive_error("move", "move", req, msg); 2549 } 2550 } 2551 else { 2552 srv_archive_error("unsupported_type", "unsupported_type", req, msg); 2553 2554 snac_debug(snac, 1, xs_fmt("process_input_message type '%s' ignored", type)); 2555 } 2556 2557 if (do_notify) { 2558 notify(snac, type, utype, actor, msg); 2559 2560 timeline_touch(snac); 2561 } 2562 2563 return 1; 2564 } 2565 2566 2567 int send_email(const xs_dict *mailinfo) 2568 /* invoke curl */ 2569 { 2570 const char *url = xs_dict_get(srv_config, "smtp_url"); 2571 2572 if (!xs_is_string(url) || *url == '\0') { 2573 /* revert back to old sendmail pipe behaviour */ 2574 const char *msg = xs_dict_get(mailinfo, "body"); 2575 FILE *f; 2576 int status = -1; 2577 int fds[2]; 2578 posix_spawn_file_actions_t facts; 2579 pid_t pid; 2580 if (pipe(fds) == -1) return -1; 2581 if (posix_spawn_file_actions_init(&facts) == 0) { 2582 if (posix_spawn_file_actions_adddup2(&facts, fds[0], 0) == 0 && 2583 posix_spawn_file_actions_addclose(&facts, fds[0]) == 0 && 2584 posix_spawn_file_actions_addclose(&facts, fds[1]) == 0) 2585 status = posix_spawn(&pid, "/usr/sbin/sendmail", &facts, NULL, 2586 (char * const []){ "sendmail", "-t", NULL }, environ); 2587 posix_spawn_file_actions_destroy(&facts); 2588 } 2589 close(fds[0]); 2590 2591 if (status != 0 || (f = fdopen(fds[1], "w")) == NULL) { 2592 close(fds[1]); 2593 return -1; 2594 } 2595 2596 fprintf(f, "%s\n", msg); 2597 fclose(f); 2598 if (waitpid(pid, &status, 0) == -1) return -1; 2599 return status; 2600 } 2601 2602 const char 2603 *user = xs_dict_get(srv_config, "smtp_username"), 2604 *pass = xs_dict_get(srv_config, "smtp_password"), 2605 *from = xs_dict_get(mailinfo, "from"), 2606 *to = xs_dict_get(mailinfo, "to"), 2607 *body = xs_dict_get(mailinfo, "body"); 2608 2609 if (url == NULL || *url == '\0') 2610 url = "smtp://localhost"; 2611 2612 int smtp_port = parse_port(url, NULL); 2613 2614 return xs_smtp_request(url, user, pass, from, to, body, smtp_port == 465 || smtp_port == 587); 2615 } 2616 2617 2618 void process_user_queue_item(snac *user, xs_dict *q_item) 2619 /* processes an item from the user queue */ 2620 { 2621 const char *type; 2622 int queue_retry_max = xs_number_get(xs_dict_get(srv_config, "queue_retry_max")); 2623 2624 if ((type = xs_dict_get(q_item, "type")) == NULL) 2625 type = "output"; 2626 2627 if (strcmp(type, "message") == 0) { 2628 const xs_dict *msg = xs_dict_get(q_item, "message"); 2629 xs *rcpts = recipient_list(user, msg, 1); 2630 xs_set inboxes; 2631 const xs_str *actor; 2632 2633 xs_set_init(&inboxes); 2634 2635 /* add this shared inbox first */ 2636 xs *this_shared_inbox = xs_fmt("%s/shared-inbox", srv_baseurl); 2637 xs_set_add(&inboxes, this_shared_inbox); 2638 enqueue_output(user, msg, this_shared_inbox, 0, 0); 2639 2640 /* iterate the recipients */ 2641 xs_list_foreach(rcpts, actor) { 2642 /* local users were served by this_shared_inbox */ 2643 if (!xs_startswith(actor, srv_baseurl)) { 2644 xs *inbox = get_actor_inbox(actor, 1); 2645 2646 if (inbox != NULL) { 2647 /* add to the set and, if it's not there, send message */ 2648 if (xs_set_add(&inboxes, inbox) == 1) 2649 enqueue_output(user, msg, inbox, 0, 0); 2650 } 2651 else 2652 snac_log(user, xs_fmt("cannot find inbox for %s", actor)); 2653 } 2654 } 2655 2656 /* if it's a public note or question, send to the collected inboxes */ 2657 if (xs_match(xs_dict_get_def(msg, "type", ""), "Create|Update") && is_msg_public(msg)) { 2658 if (xs_type(xs_dict_get(srv_config, "disable_inbox_collection")) != XSTYPE_TRUE) { 2659 xs *shibx = inbox_list(); 2660 const xs_str *inbox; 2661 2662 xs_list_foreach(shibx, inbox) { 2663 if (xs_set_add(&inboxes, inbox) == 1) 2664 enqueue_output(user, msg, inbox, 0, 0); 2665 } 2666 } 2667 } 2668 2669 xs_set_free(&inboxes); 2670 2671 /* relay this note */ 2672 if (is_msg_public(msg) && strcmp(user->uid, "relay") != 0) { /* avoid loops */ 2673 snac relay; 2674 if (user_open(&relay, "relay")) { 2675 /* a 'relay' user exists */ 2676 const char *type = xs_dict_get(msg, "type"); 2677 2678 if (xs_is_string(type) && strcmp(type, "Create") == 0) { 2679 const xs_val *object = xs_dict_get(msg, "object"); 2680 2681 if (xs_is_dict(object)) { 2682 object = xs_dict_get(object, "id"); 2683 2684 snac_debug(&relay, 1, xs_fmt("relaying message %s", object)); 2685 2686 xs *boost = msg_admiration(&relay, object, "Announce"); 2687 enqueue_message(&relay, boost); 2688 } 2689 } 2690 2691 user_free(&relay); 2692 } 2693 } 2694 } 2695 else 2696 if (strcmp(type, "input") == 0) { 2697 /* process the message */ 2698 const xs_dict *msg = xs_dict_get(q_item, "message"); 2699 const xs_dict *req = xs_dict_get(q_item, "req"); 2700 int retries = xs_number_get(xs_dict_get(q_item, "retries")); 2701 2702 if (xs_is_null(msg)) 2703 return; 2704 2705 if (!process_input_message(user, msg, req)) { 2706 if (retries > queue_retry_max) 2707 snac_log(user, xs_fmt("input giving up")); 2708 else { 2709 /* reenqueue */ 2710 enqueue_input(user, msg, req, retries + 1); 2711 snac_log(user, xs_fmt("input requeue #%d", retries + 1)); 2712 } 2713 } 2714 } 2715 else 2716 if (strcmp(type, "close_question") == 0) { 2717 /* the time for this question has ended */ 2718 const char *id = xs_dict_get(q_item, "message"); 2719 2720 if (!xs_is_null(id)) 2721 update_question(user, id); 2722 } 2723 else 2724 if (strcmp(type, "object_request") == 0) { 2725 const char *id = xs_dict_get(q_item, "message"); 2726 2727 if (!xs_is_null(id)) { 2728 int status; 2729 xs *data = NULL; 2730 2731 status = activitypub_request(user, id, &data); 2732 2733 if (valid_status(status)) 2734 object_add_ow(id, data); 2735 2736 snac_debug(user, 1, xs_fmt("object_request %s %d", id, status)); 2737 } 2738 } 2739 else 2740 if (strcmp(type, "verify_links") == 0) { 2741 verify_links(user); 2742 } 2743 else 2744 if (strcmp(type, "actor_refresh") == 0) { 2745 const char *actor = xs_dict_get(q_item, "actor"); 2746 double mtime = object_mtime(actor); 2747 2748 /* only refresh if it was refreshed more than an hour ago */ 2749 if (mtime + 3600.0 < (double) time(NULL)) { 2750 xs *actor_o = NULL; 2751 int status; 2752 2753 if (valid_status((status = activitypub_request(user, actor, &actor_o)))) 2754 actor_add(actor, actor_o); 2755 else 2756 object_touch(actor); 2757 2758 snac_log(user, xs_fmt("actor_refresh %s %d", actor, status)); 2759 } 2760 } 2761 else 2762 if (strcmp(type, "notify_webhook") == 0) { 2763 const char *webhook = xs_dict_get(user->config, "notify_webhook"); 2764 2765 if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/ 2766 const xs_dict *msg = xs_dict_get(q_item, "message"); 2767 int retries = xs_number_get(xs_dict_get(q_item, "retries")); 2768 2769 xs *hdrs = xs_dict_new(); 2770 2771 hdrs = xs_dict_set(hdrs, "content-type", "application/json"); 2772 hdrs = xs_dict_set(hdrs, "user-agent", USER_AGENT); 2773 2774 xs *body = xs_json_dumps(msg, 4); 2775 2776 int status; 2777 xs *rsp = xs_http_request("POST", webhook, hdrs, body, strlen(body), &status, NULL, NULL, 0); 2778 2779 snac_debug(user, 0, xs_fmt("webhook post %s %d", webhook, status)); 2780 2781 if (!valid_status(status)) { 2782 retries++; 2783 2784 if (retries > queue_retry_max) 2785 snac_debug(user, 0, xs_fmt("webhook post giving up %s", webhook)); 2786 else { 2787 snac_debug(user, 0, xs_fmt("webhook post requeue %s %d", webhook, retries)); 2788 2789 enqueue_notify_webhook(user, msg, retries); 2790 } 2791 } 2792 } 2793 } 2794 else 2795 snac_log(user, xs_fmt("unexpected user q_item type '%s'", type)); 2796 } 2797 2798 2799 int process_user_queue(snac *snac) 2800 /* processes a user's queue */ 2801 { 2802 int cnt = 0; 2803 xs *list = user_queue(snac); 2804 2805 xs_list *p = list; 2806 const xs_str *fn; 2807 2808 while (xs_list_iter(&p, &fn)) { 2809 xs *q_item = dequeue(fn); 2810 2811 if (q_item == NULL) 2812 continue; 2813 2814 process_user_queue_item(snac, q_item); 2815 cnt++; 2816 } 2817 2818 scheduled_process(snac); 2819 2820 return cnt; 2821 } 2822 2823 2824 xs_str *str_status(int status) 2825 { 2826 return xs_fmt("%d %s", status, status < 0 ? xs_curl_strerr(status) : http_status_text(status)); 2827 } 2828 2829 2830 void process_queue_item(xs_dict *q_item) 2831 /* processes an item from the global queue */ 2832 { 2833 const char *type = xs_dict_get(q_item, "type"); 2834 int queue_retry_max = xs_number_get(xs_dict_get(srv_config, "queue_retry_max")); 2835 2836 if (strcmp(type, "output") == 0) { 2837 int status; 2838 const xs_str *inbox = xs_dict_get(q_item, "inbox"); 2839 const xs_str *keyid = xs_dict_get(q_item, "keyid"); 2840 const xs_str *seckey = xs_dict_get(q_item, "seckey"); 2841 const xs_dict *msg = xs_dict_get(q_item, "message"); 2842 int retries = xs_number_get(xs_dict_get(q_item, "retries")); 2843 int p_status = xs_number_get(xs_dict_get(q_item, "p_status")); 2844 xs *payload = NULL; 2845 int p_size = 0; 2846 int timeout = 0; 2847 2848 if (xs_is_null(inbox) || xs_is_null(msg) || xs_is_null(keyid) || xs_is_null(seckey)) { 2849 srv_log(xs_fmt("output message error: missing fields")); 2850 return; 2851 } 2852 2853 if (is_instance_blocked(inbox)) { 2854 srv_debug(0, xs_fmt("discarded output message to blocked instance %s", inbox)); 2855 return; 2856 } 2857 2858 /* deliver (if previous error status was a timeout, try now longer) */ 2859 if (p_status == 599) 2860 timeout = xs_number_get(xs_dict_get_def(srv_config, "queue_timeout_2", "8")); 2861 else 2862 timeout = xs_number_get(xs_dict_get_def(srv_config, "queue_timeout", "6")); 2863 2864 if (timeout == 0) 2865 timeout = 6; 2866 2867 status = send_to_inbox_raw(keyid, seckey, inbox, msg, &payload, &p_size, timeout); 2868 2869 if (payload) { 2870 if (p_size > 64) { 2871 /* trim the message */ 2872 payload[64] = '\0'; 2873 payload = xs_str_cat(payload, "..."); 2874 } 2875 2876 /* strip ugly control characters */ 2877 payload = xs_replace_i(payload, "\n", ""); 2878 payload = xs_replace_i(payload, "\r", ""); 2879 2880 if (*payload) 2881 payload = xs_str_wrap_i(" [", payload, "]"); 2882 } 2883 else 2884 payload = xs_str_new(NULL); 2885 2886 xs *s_status = str_status(status); 2887 2888 srv_log(xs_fmt("output message: sent to inbox %s (%s)%s", inbox, s_status, payload)); 2889 2890 if (!valid_status(status)) { 2891 retries++; 2892 2893 /* if it's not the first time it fails with a timeout, 2894 penalize the server by skipping one retry */ 2895 if (p_status == status && status == HTTP_STATUS_CLIENT_CLOSED_REQUEST) 2896 retries++; 2897 2898 /* error sending; requeue? */ 2899 if (status == HTTP_STATUS_BAD_REQUEST 2900 || status == HTTP_STATUS_NOT_FOUND 2901 || status == HTTP_STATUS_METHOD_NOT_ALLOWED 2902 || status == HTTP_STATUS_GONE 2903 || status == HTTP_STATUS_UNPROCESSABLE_CONTENT 2904 || status < 0) 2905 /* explicit error: discard */ 2906 srv_log(xs_fmt("output message: error %s (%s)", inbox, s_status)); 2907 else 2908 if (retries > queue_retry_max) 2909 srv_log(xs_fmt("output message: giving up %s (%s)", inbox, s_status)); 2910 else { 2911 /* requeue */ 2912 enqueue_output_raw(keyid, seckey, msg, inbox, retries, status); 2913 srv_log(xs_fmt("output message: requeue %s #%d", inbox, retries)); 2914 } 2915 } 2916 } 2917 else 2918 if (strcmp(type, "email") == 0) { 2919 /* send this email */ 2920 const xs_dict *msg = xs_dict_get(q_item, "message"); 2921 int retries = xs_number_get(xs_dict_get(q_item, "retries")); 2922 2923 if (!send_email(msg)) 2924 srv_debug(1, xs_fmt("email message sent")); 2925 else { 2926 retries++; 2927 2928 if (retries > queue_retry_max) 2929 srv_log(xs_fmt("email giving up (errno: %d)", errno)); 2930 else { 2931 /* requeue */ 2932 srv_log(xs_fmt( 2933 "email requeue #%d (errno: %d)", retries, errno)); 2934 2935 enqueue_email(msg, retries); 2936 } 2937 } 2938 } 2939 else 2940 if (strcmp(type, "telegram") == 0) { 2941 /* send this via telegram */ 2942 const char *bot = xs_dict_get(q_item, "bot"); 2943 const char *msg = xs_dict_get(q_item, "message"); 2944 xs *chat_id = xs_dup(xs_dict_get(q_item, "chat_id")); 2945 int status = 0; 2946 2947 /* chat_id must start with a - */ 2948 if (!xs_startswith(chat_id, "-")) 2949 chat_id = xs_str_wrap_i("-", chat_id, NULL); 2950 2951 xs *url = xs_fmt("https:/" "/api.telegram.org/bot%s/sendMessage", bot); 2952 xs *body = xs_fmt("{\"chat_id\":%s,\"text\":\"%s\"}", chat_id, msg); 2953 2954 xs *headers = xs_dict_new(); 2955 headers = xs_dict_append(headers, "content-type", "application/json"); 2956 2957 xs *rsp = xs_http_request("POST", url, headers, 2958 body, strlen(body), &status, NULL, NULL, 0); 2959 rsp = xs_free(rsp); 2960 2961 srv_debug(0, xs_fmt("telegram post %d", status)); 2962 } 2963 else 2964 if (strcmp(type, "ntfy") == 0) { 2965 /* send this via ntfy */ 2966 const char *ntfy_server = xs_dict_get(q_item, "ntfy_server"); 2967 const char *msg = xs_dict_get(q_item, "message"); 2968 const char *ntfy_token = xs_dict_get(q_item, "ntfy_token"); 2969 int status = 0; 2970 2971 xs *url = xs_fmt("%s", ntfy_server); 2972 xs *body = xs_fmt("%s", msg); 2973 2974 xs *headers = xs_dict_new(); 2975 headers = xs_dict_append(headers, "content-type", "text/plain"); 2976 // Append the Authorization header only if ntfy_token is not NULL 2977 if (ntfy_token != NULL) { 2978 headers = xs_dict_append(headers, "Authorization", xs_fmt("Bearer %s", ntfy_token)); 2979 } 2980 2981 xs *rsp = xs_http_request("POST", url, headers, 2982 body, strlen(body), &status, NULL, NULL, 0); 2983 rsp = xs_free(rsp); 2984 2985 srv_debug(0, xs_fmt("ntfy post %d", status)); 2986 } 2987 else 2988 if (strcmp(type, "purge") == 0) { 2989 srv_log(xs_dup("purge start")); 2990 2991 purge_all(); 2992 2993 srv_log(xs_dup("purge end")); 2994 } 2995 else 2996 if (strcmp(type, "input") == 0) { 2997 const xs_dict *msg = xs_dict_get(q_item, "message"); 2998 const xs_dict *req = xs_dict_get(q_item, "req"); 2999 int retries = xs_number_get(xs_dict_get(q_item, "retries")); 3000 3001 /* do some instance-level checks */ 3002 int r = process_input_message(NULL, msg, req); 3003 3004 if (r == 0) { 3005 /* transient error? retry */ 3006 if (retries > queue_retry_max) 3007 srv_log(xs_fmt("shared input giving up")); 3008 else { 3009 /* reenqueue */ 3010 enqueue_shared_input(msg, req, retries + 1); 3011 srv_log(xs_fmt("shared input requeue #%d", retries + 1)); 3012 } 3013 } 3014 else 3015 if (r == 2) { 3016 /* redistribute the input message to all users */ 3017 const char *ntid = xs_dict_get(q_item, "ntid"); 3018 xs *tmpfn = xs_fmt("%s/tmp/%s.json", srv_basedir, ntid); 3019 FILE *f; 3020 3021 if ((f = fopen(tmpfn, "w")) != NULL) { 3022 xs_json_dump(q_item, 4, f); 3023 fclose(f); 3024 } 3025 3026 xs *users = user_list(); 3027 xs_list *p = users; 3028 const char *v; 3029 int cnt = 0; 3030 3031 while (xs_list_iter(&p, &v)) { 3032 snac user; 3033 3034 if (user_open(&user, v)) { 3035 int rsn = is_msg_for_me(&user, msg); 3036 if (rsn) { 3037 xs *fn = xs_fmt("%s/queue/%s.json", user.basedir, ntid); 3038 3039 snac_debug(&user, 1, 3040 xs_fmt("enqueue_input (from shared inbox) %s [%d]", xs_dict_get(msg, "id"), rsn)); 3041 3042 if (link(tmpfn, fn) < 0) 3043 srv_log(xs_fmt("link(%s, %s) error", tmpfn, fn)); 3044 3045 cnt++; 3046 } 3047 3048 user_free(&user); 3049 } 3050 } 3051 3052 unlink(tmpfn); 3053 3054 if (cnt == 0) { 3055 srv_debug(1, xs_fmt("no valid recipients for %s", xs_dict_get(msg, "id"))); 3056 } 3057 } 3058 } 3059 else 3060 if (strcmp(type, "webmention") == 0) { 3061 const xs_dict *msg = xs_dict_get(q_item, "message"); 3062 const char *source = xs_dict_get(msg, "id"); 3063 const char *content = xs_dict_get(msg, "content"); 3064 3065 if (xs_is_string(source) && xs_is_string(content)) { 3066 xs *links = xs_regex_select(content, "\"https?:/" "/[^\"]+"); 3067 const char *link; 3068 3069 xs_list_foreach(links, link) { 3070 xs *target = xs_strip_chars_i(xs_dup(link), "\""); 3071 3072 int r = xs_webmention_send(source, target, USER_AGENT); 3073 3074 srv_debug(1, xs_fmt("webmention source=%s target=%s %d", source, target, r)); 3075 } 3076 } 3077 } 3078 else 3079 if (strcmp(type, "rss_hashtag_poll") == 0) { 3080 rss_poll_hashtags(); 3081 } 3082 else 3083 srv_log(xs_fmt("unexpected q_item type '%s'", type)); 3084 } 3085 3086 3087 int process_queue(void) 3088 /* processes the global queue */ 3089 { 3090 int cnt = 0; 3091 xs *list = queue(); 3092 3093 xs_list *p = list; 3094 const xs_str *fn; 3095 3096 while (xs_list_iter(&p, &fn)) { 3097 xs *q_item = dequeue(fn); 3098 3099 if (q_item != NULL) { 3100 job_post(q_item, 0); 3101 cnt++; 3102 } 3103 } 3104 3105 return cnt; 3106 } 3107 3108 3109 /** account migration **/ 3110 3111 int migrate_account(snac *user) 3112 /* migrates this account to a new one (stored in the 'alias' user field) */ 3113 { 3114 const char *new_account = xs_dict_get(user->config, "alias"); 3115 3116 if (xs_type(new_account) != XSTYPE_STRING) { 3117 snac_log(user, xs_fmt("Cannot migrate: alias (destination account) not set")); 3118 return 1; 3119 } 3120 3121 xs *new_actor = NULL; 3122 int status; 3123 3124 if (!valid_status(status = activitypub_request(user, new_account, &new_actor))) { 3125 snac_log(user, xs_fmt("Cannot migrate: error requesting actor %s %d", new_account, status)); 3126 return 1; 3127 } 3128 3129 actor_add(new_account, new_actor); 3130 3131 const char *loaka = xs_dict_get(new_actor, "alsoKnownAs"); 3132 3133 if (xs_type(loaka) != XSTYPE_LIST) { 3134 snac_log(user, xs_fmt("Cannot migrate: destination account doesn't have any aliases")); 3135 return 1; 3136 } 3137 3138 if (xs_list_in(loaka, user->actor) == -1) { 3139 snac_log(user, xs_fmt("Cannot migrate: destination account doesn't have this one as an alias")); 3140 return 1; 3141 } 3142 3143 xs *move = msg_move(user, new_account); 3144 xs *fwers = follower_list(user); 3145 3146 const char *actor; 3147 xs_list_foreach(fwers, actor) { 3148 /* get the actor inbox, excluding the shared one */ 3149 xs *inbox = get_actor_inbox(actor, 0); 3150 3151 if (xs_is_null(inbox)) 3152 snac_log(user, xs_fmt("migrate_account: cannot get inbox for actor %s", actor)); 3153 else 3154 enqueue_output(user, move, inbox, 0, 0); 3155 } 3156 3157 return 0; 3158 } 3159 3160 3161 /** HTTP handlers */ 3162 3163 int activitypub_get_handler(const xs_dict *req, const char *q_path, 3164 char **body, int *b_size, char **ctype) 3165 { 3166 int status = HTTP_STATUS_OK; 3167 const char *accept = xs_dict_get(req, "accept"); 3168 snac snac; 3169 xs *msg = NULL; 3170 3171 if (accept == NULL) 3172 return 0; 3173 3174 if (xs_str_in(accept, "application/activity+json") == -1 && 3175 xs_str_in(accept, "application/ld+json") == -1) 3176 return 0; 3177 3178 xs *l = xs_split_n(q_path, "/", 2); 3179 const char *uid; 3180 const char *p_path; 3181 3182 uid = xs_list_get(l, 1); 3183 if (!user_open(&snac, uid)) { 3184 /* invalid user */ 3185 srv_debug(1, xs_fmt("activitypub_get_handler bad user %s", uid)); 3186 return HTTP_STATUS_NOT_FOUND; 3187 } 3188 3189 p_path = xs_list_get(l, 2); 3190 3191 *ctype = "application/activity+json"; 3192 3193 int show_contact_metrics = xs_is_true(xs_dict_get(snac.config, "show_contact_metrics")); 3194 3195 if (p_path == NULL) { 3196 /* if there was no component after the user, it's an actor request */ 3197 msg = msg_actor(&snac); 3198 *ctype = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""; 3199 3200 const char *ua = xs_dict_get(req, "user-agent"); 3201 3202 snac_debug(&snac, 0, xs_fmt("serving actor [%s]", ua ? ua : "No UA")); 3203 } 3204 else 3205 if (strcmp(p_path, "outbox") == 0 || strcmp(p_path, "featured") == 0) { 3206 xs *id = xs_fmt("%s/%s", snac.actor, p_path); 3207 xs *list = xs_list_new(); 3208 const char *v; 3209 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); 3210 3211 /* get the public outbox or the pinned list */ 3212 xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt, NULL) : pinned_list(&snac); 3213 3214 xs_list_foreach(elems, v) { 3215 xs *i = NULL; 3216 3217 if (valid_status(object_get_by_md5(v, &i))) { 3218 const char *type = xs_dict_get(i, "type"); 3219 const char *id = xs_dict_get(i, "id"); 3220 3221 if (type && id && strcmp(type, "Note") == 0 && xs_startswith(id, snac.actor)) { 3222 xs *c_msg = msg_create(&snac, i); 3223 list = xs_list_append(list, c_msg); 3224 } 3225 } 3226 } 3227 3228 /* replace the 'orderedItems' with the latest posts */ 3229 msg = msg_collection(&snac, id, xs_list_len(list)); 3230 msg = xs_dict_set(msg, "orderedItems", list); 3231 } 3232 else 3233 if (strcmp(p_path, "followers") == 0) { 3234 int total = 0; 3235 3236 if (show_contact_metrics) { 3237 total = follower_list_len(&snac); 3238 } 3239 3240 xs *id = xs_fmt("%s/%s", snac.actor, p_path); 3241 msg = msg_collection(&snac, id, total); 3242 } 3243 else 3244 if (strcmp(p_path, "following") == 0) { 3245 int total = 0; 3246 3247 if (show_contact_metrics) { 3248 total = following_list_len(&snac); 3249 } 3250 3251 xs *id = xs_fmt("%s/%s", snac.actor, p_path); 3252 msg = msg_collection(&snac, id, total); 3253 } 3254 else 3255 if (xs_startswith(p_path, "p/")) { 3256 xs *id = xs_fmt("%s/%s", snac.actor, p_path); 3257 3258 status = object_get(id, &msg); 3259 3260 /* don't return non-public objects */ 3261 if (valid_status(status) && !is_msg_public(msg)) 3262 status = HTTP_STATUS_NOT_FOUND; 3263 } 3264 else 3265 status = HTTP_STATUS_NOT_FOUND; 3266 3267 if (status == HTTP_STATUS_OK && msg != NULL) { 3268 *body = xs_json_dumps(msg, 4); 3269 *b_size = strlen(*body); 3270 } 3271 3272 snac_debug(&snac, 1, xs_fmt("activitypub_get_handler serving %s %d", q_path, status)); 3273 3274 user_free(&snac); 3275 3276 return status; 3277 } 3278 3279 3280 int activitypub_post_handler(const xs_dict *req, const char *q_path, 3281 char *payload, int p_size, 3282 char **body, int *b_size, char **ctype) 3283 /* processes an input message */ 3284 { 3285 (void)b_size; 3286 3287 int status = HTTP_STATUS_ACCEPTED; 3288 const char *i_ctype = xs_dict_get(req, "content-type"); 3289 snac snac; 3290 const char *v; 3291 3292 if (i_ctype == NULL) { 3293 *body = xs_str_new("no content-type"); 3294 *ctype = "text/plain"; 3295 return HTTP_STATUS_BAD_REQUEST; 3296 } 3297 3298 if (xs_is_null(payload)) { 3299 *body = xs_str_new("no payload"); 3300 *ctype = "text/plain"; 3301 return HTTP_STATUS_BAD_REQUEST; 3302 } 3303 3304 if (xs_str_in(i_ctype, "application/activity+json") == -1 && 3305 xs_str_in(i_ctype, "application/ld+json") == -1) 3306 return 0; 3307 3308 /* decode the message */ 3309 xs *msg = xs_json_loads(payload); 3310 const char *id = xs_dict_get(msg, "id"); 3311 3312 if (msg == NULL) { 3313 srv_log(xs_fmt("activitypub_post_handler JSON error %s", q_path)); 3314 3315 srv_archive_error("activitypub_post_handler", "JSON error", req, payload); 3316 3317 *body = xs_str_new("JSON error"); 3318 *ctype = "text/plain"; 3319 return HTTP_STATUS_BAD_REQUEST; 3320 } 3321 3322 if (id && is_instance_blocked(id)) { 3323 srv_debug(1, xs_fmt("full instance block for %s", id)); 3324 3325 *body = xs_str_new("blocked"); 3326 *ctype = "text/plain"; 3327 return HTTP_STATUS_FORBIDDEN; 3328 } 3329 3330 /* get the user and path */ 3331 xs *l = xs_split_n(q_path, "/", 2); 3332 3333 if (xs_list_len(l) == 2 && strcmp(xs_list_get(l, 1), "shared-inbox") == 0) { 3334 enqueue_shared_input(msg, req, 0); 3335 return HTTP_STATUS_ACCEPTED; 3336 } 3337 3338 if (xs_list_len(l) != 3 || strcmp(xs_list_get(l, 2), "inbox") != 0) { 3339 /* strange q_path */ 3340 srv_debug(1, xs_fmt("activitypub_post_handler unsupported path %s", q_path)); 3341 return HTTP_STATUS_NOT_FOUND; 3342 } 3343 3344 const char *uid = xs_list_get(l, 1); 3345 if (!user_open(&snac, uid)) { 3346 /* invalid user */ 3347 srv_debug(1, xs_fmt("activitypub_post_handler bad user %s", uid)); 3348 return HTTP_STATUS_NOT_FOUND; 3349 } 3350 3351 /* if it has a digest, check it now, because 3352 later the payload won't be exactly the same */ 3353 if ((v = xs_dict_get(req, "digest")) != NULL) { 3354 xs *s1 = xs_sha256_base64(payload, p_size); 3355 xs *s2 = xs_fmt("SHA-256=%s", s1); 3356 3357 if (strcmp(s2, v) != 0) { 3358 srv_log(xs_fmt("digest check FAILED")); 3359 3360 *body = xs_str_new("bad digest"); 3361 *ctype = "text/plain"; 3362 status = HTTP_STATUS_BAD_REQUEST; 3363 } 3364 } 3365 3366 /* if the message is from a muted actor, reject it right now */ 3367 if (!xs_is_null(v = xs_dict_get(msg, "actor")) && *v) { 3368 if (is_muted(&snac, v)) { 3369 snac_log(&snac, xs_fmt("rejected message from MUTEd actor %s", v)); 3370 3371 *body = xs_str_new("rejected"); 3372 *ctype = "text/plain"; 3373 status = HTTP_STATUS_FORBIDDEN; 3374 } 3375 } 3376 3377 if (valid_status(status)) { 3378 enqueue_input(&snac, msg, req, 0); 3379 *ctype = "application/activity+json"; 3380 } 3381 3382 user_free(&snac); 3383 3384 return status; 3385 }