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