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