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