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