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