main
activitypub.c
1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3
4#include "xs.h"
5#include "xs_json.h"
6#include "xs_curl.h"
7#include "xs_url.h"
8#include "xs_mime.h"
9#include "xs_bearssl.h"
10#include "xs_regex.h"
11#include "xs_time.h"
12#include "xs_set.h"
13#include "xs_match.h"
14#include "xs_unicode.h"
15#include "xs_webmention.h"
16#include "xs_http.h"
17
18#include "snac.h"
19
20#include <stddef.h>
21#include <sys/wait.h>
22
23const char * const public_address = "https:/" "/www.w3.org/ns/activitystreams#Public";
24
25/* susie.png */
26
27const char * const susie =
28 "iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAQAAAAC"
29 "CEkxzAAAAUUlEQVQoz43R0QkAMQwCUDdw/y3dwE"
30 "vsvzlL4X1IoQkAisKmwfAFT3RgJHbQezpSRoXEq"
31 "eqCL9BJBf7h3QbOCCxV5EVWMEMwG7K1/WODtlvx"
32 "AYTtEsDU9F34AAAAAElFTkSuQmCC";
33
34const char * const susie_cool =
35 "iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAQAAAAC"
36 "CEkxzAAAAV0lEQVQoz43RwQ3AMAwCQDZg/y3ZgN"
37 "qo3+JaedwDOUQBQFHYaTB8wTM6sGl2cMPu+DFzn"
38 "+ZcgN7wF7ZVihXkfSlWIVzIA6dbQzaygllpNuTX"
39 "ZmmFNlvxADX1+o0cUPMbAAAAAElFTkSuQmCC";
40
41const char * const susie_muertos =
42 "iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAQAAAAC"
43 "CEkxzAAAAV0lEQVQoz4XQsQ0AMQxCUW/A/lv+DT"
44 "ic6zGRolekIMyMELNp8PiCEw6Q4w4NoAt53IH5m"
45 "xXksrZYgZwJrIox+Z8vJAfe2lCxG6AK7eKkWcEb"
46 "QHbF617xAQatAAD7jJHUAAAAAElFTkSuQmCC";
47
48
49const char *default_avatar_base64(void)
50/* returns the default avatar in base64 */
51{
52 time_t t = time(NULL);
53 struct tm tm;
54 const char *p = susie;
55
56 gmtime_r(&t, &tm);
57
58 if (tm.tm_mon == 10 && tm.tm_mday == 2)
59 p = susie_muertos;
60 else
61 if (tm.tm_wday == 0 || tm.tm_wday == 6)
62 p = susie_cool;
63
64 return p;
65}
66
67
68int activitypub_request(snac *user, const char *url, xs_dict **data)
69/* request an object */
70{
71 int status = 0;
72 xs *response = NULL;
73 xs *payload = NULL;
74 int p_size;
75 const char *ctype;
76
77 *data = NULL;
78
79 if (user != NULL) {
80 /* get from the net */
81 response = http_signed_request(user, "GET", url,
82 NULL, NULL, 0, &status, &payload, &p_size, 0);
83 }
84
85 if (status == 0 || (status >= 500 && status <= 599)) {
86 /* I found an instance running Misskey that returned
87 500 on signed messages but returned the object
88 perfectly without signing (?), so why not try */
89 xs_free(response);
90
91 xs *hdrs = xs_dict_new();
92 hdrs = xs_dict_append(hdrs, "accept", "application/activity+json");
93 hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT);
94
95 response = xs_http_request("GET", url, hdrs,
96 NULL, 0, &status, &payload, &p_size, 0);
97 }
98
99 if (p_size >= 100000)
100 return HTTP_STATUS_BAD_REQUEST;
101
102 if (valid_status(status)) {
103 /* ensure it's ActivityPub data */
104 ctype = xs_dict_get(response, "content-type");
105
106 if (xs_is_null(ctype))
107 status = HTTP_STATUS_BAD_REQUEST;
108 else
109 if (xs_str_in(ctype, "application/activity+json") != -1 ||
110 xs_str_in(ctype, "application/ld+json") != -1) {
111
112 /* if there is no payload, fail */
113 if (xs_is_null(payload))
114 status = HTTP_STATUS_BAD_REQUEST;
115 else
116 *data = xs_json_loads(payload);
117 }
118 else
119 status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
120 }
121
122 return status;
123}
124
125
126static xs_dict *actor_get_collections(snac *user, xs_dict *actor, int throttle)
127/* fetches follower/following/statuses counts from an actor's collections and adds them to the actor object */
128{
129 /* only fetch if counts are not already present in the actor object */
130 const xs_number *existing_followers = xs_dict_get(actor, "followers_count");
131 const xs_number *existing_following = xs_dict_get(actor, "following_count");
132 const xs_number *existing_statuses = xs_dict_get(actor, "statuses_count");
133
134 /* skip if we already have all counts */
135 if (xs_type(existing_followers) == XSTYPE_NUMBER &&
136 xs_type(existing_following) == XSTYPE_NUMBER &&
137 xs_type(existing_statuses) == XSTYPE_NUMBER) {
138 return actor;
139 }
140
141 /* CRITICAL: duplicate URLs BEFORE any xs_dict_set calls, as xs_dict_set can reallocate the dict */
142 xs *followers_url = xs_dup(xs_dict_get(actor, "followers"));
143 xs *following_url = xs_dup(xs_dict_get(actor, "following"));
144 xs *outbox_url = xs_dup(xs_dict_get(actor, "outbox"));
145
146 /* only fetch followers count if not already present */
147 if (xs_type(existing_followers) != XSTYPE_NUMBER && !xs_is_null(followers_url)) {
148 xs *followers_coll = NULL;
149 if (valid_status(activitypub_request(user, followers_url, &followers_coll))) {
150 const xs_number *total = xs_dict_get(followers_coll, "totalItems");
151 if (xs_type(total) == XSTYPE_NUMBER) {
152 xs *total_copy = xs_dup(total);
153 actor = xs_dict_set(actor, "followers_count", total_copy);
154 }
155 }
156 /* throttle to prevent resource exhaustion on low-power devices */
157 if (throttle)
158 usleep(100000); /* 100ms delay between requests */
159 }
160
161 /* only fetch following count if not already present */
162 if (xs_type(existing_following) != XSTYPE_NUMBER && !xs_is_null(following_url)) {
163 xs *following_coll = NULL;
164 if (valid_status(activitypub_request(user, following_url, &following_coll))) {
165 const xs_number *total = xs_dict_get(following_coll, "totalItems");
166 if (xs_type(total) == XSTYPE_NUMBER) {
167 xs *total_copy = xs_dup(total);
168 actor = xs_dict_set(actor, "following_count", total_copy);
169 }
170 }
171 /* throttle to prevent resource exhaustion on low-power devices */
172 if (throttle)
173 usleep(100000); /* 100ms delay between requests */
174 }
175
176 /* only fetch statuses count if not already present */
177 if (xs_type(existing_statuses) != XSTYPE_NUMBER && !xs_is_null(outbox_url)) {
178 xs *outbox_coll = NULL;
179 if (valid_status(activitypub_request(user, outbox_url, &outbox_coll))) {
180 const xs_number *total = xs_dict_get(outbox_coll, "totalItems");
181 if (xs_type(total) == XSTYPE_NUMBER) {
182 xs *total_copy = xs_dup(total);
183 actor = xs_dict_set(actor, "statuses_count", total_copy);
184 }
185 }
186 /* throttle to prevent resource exhaustion on low-power devices */
187 if (throttle)
188 usleep(100000); /* 100ms delay between requests */
189 }
190
191 return actor;
192}
193
194
195int actor_request(snac *user, const char *actor, xs_dict **data)
196/* request an actor */
197{
198 int status;
199 xs *payload = NULL;
200
201 if (data)
202 *data = NULL;
203
204 /* get from disk first */
205 status = actor_get_refresh(user, actor, data);
206
207 if (!valid_status(status)) {
208 /* actor data non-existent: get from the net */
209 status = activitypub_request(user, actor, &payload);
210
211 if (valid_status(status)) {
212 /* fetch collection counts when initially fetching an actor (no throttle) */
213 payload = actor_get_collections(user, payload, 0);
214
215 /* renew data */
216 status = actor_add(actor, payload);
217
218 if (data != NULL) {
219 *data = payload;
220 payload = NULL;
221 }
222 }
223 else
224 srv_debug(1, xs_fmt("actor_request error %s %d", actor, status));
225 }
226
227 /* collect the (presumed) shared inbox in this actor */
228 if (xs_type(xs_dict_get(srv_config, "disable_inbox_collection")) != XSTYPE_TRUE) {
229 if (valid_status(status) && data && *data)
230 inbox_add_by_actor(*data);
231 }
232
233 return status;
234}
235
236
237const char *get_atto(const xs_dict *msg)
238/* gets the attributedTo field (an actor) */
239{
240 const xs_val *actor = xs_dict_get(msg, "attributedTo");
241
242 /* if the actor is a list of objects (like on Peertube videos), pick the Person */
243 if (xs_type(actor) == XSTYPE_LIST) {
244 const xs_list *p = actor;
245 int c = 0;
246 const xs_dict *v;
247 actor = NULL;
248
249 while (actor == NULL && xs_list_next(p, &v, &c)) {
250 if (xs_type(v) == XSTYPE_DICT) {
251 const char *type = xs_dict_get(v, "type");
252 if (xs_type(type) == XSTYPE_STRING && strcmp(type, "Person") == 0) {
253 actor = xs_dict_get(v, "id");
254
255 if (xs_type(actor) != XSTYPE_STRING)
256 actor = NULL;
257 }
258 }
259 }
260 }
261 else
262 if (xs_type(actor) == XSTYPE_DICT) {
263 /* bandwagon.fm returns this */
264 actor = xs_dict_get(actor, "id");
265 }
266
267 return actor;
268}
269
270
271const char *get_in_reply_to(const xs_dict *msg)
272/* gets the inReplyTo id */
273{
274 const xs_val *in_reply_to = xs_dict_get(msg, "inReplyTo");
275
276 if (xs_type(in_reply_to) == XSTYPE_DICT)
277 in_reply_to = xs_dict_get(in_reply_to, "id");
278
279 return in_reply_to;
280}
281
282
283xs_list *get_attachments(const xs_dict *msg)
284/* unify the garbage fire that are the attachments */
285{
286 xs_list *l = xs_list_new();
287 const xs_list *p;
288
289 /* try first the attachments list */
290 if (!xs_is_null(p = xs_dict_get(msg, "attachment"))) {
291 xs *attach = NULL;
292 const xs_val *v;
293
294 /* ensure it's a list */
295 if (xs_type(p) == XSTYPE_DICT) {
296 attach = xs_list_new();
297 attach = xs_list_append(attach, p);
298 }
299 else
300 attach = xs_dup(p);
301
302 if (xs_type(attach) == XSTYPE_LIST && xs_list_len(attach) == 0) {
303 /* does the message have an image? */
304 const xs_dict *d = xs_dict_get(msg, "image");
305 if (xs_type(d) == XSTYPE_DICT) {
306 /* add it to the attachment list */
307 attach = xs_list_append(attach, d);
308 }
309 }
310
311 /* now iterate the list */
312 int c = 0;
313 while (xs_list_next(attach, &v, &c)) {
314 const char *type = xs_dict_get(v, "mediaType");
315 if (xs_is_null(type))
316 type = xs_dict_get(v, "type");
317
318 if (xs_is_null(type))
319 continue;
320
321 const char *href = xs_dict_get(v, "url");
322 if (xs_is_null(href))
323 href = xs_dict_get(v, "href");
324 if (xs_is_null(href))
325 continue;
326
327 /* infer MIME type from non-specific attachments */
328 if (xs_list_len(attach) < 2 && xs_match(type, "Link|Document")) {
329 char *mt = (char *)xs_mime_by_ext(href);
330
331 if (xs_match(mt, "image/*|audio/*|video/*")) /* */
332 type = mt;
333 }
334
335 const char *name = xs_dict_get(v, "name");
336 if (xs_is_null(name))
337 name = xs_dict_get(msg, "name");
338 if (xs_is_null(name))
339 name = "";
340
341 xs *d = xs_dict_new();
342 d = xs_dict_append(d, "type", type);
343 d = xs_dict_append(d, "href", href);
344 d = xs_dict_append(d, "name", name);
345
346 const xs_dict *icon = xs_dict_get(v, "icon");
347 if (xs_type(icon) == XSTYPE_DICT)
348 d = xs_dict_append(d, "icon", icon);
349
350 l = xs_list_append(l, d);
351 }
352 }
353
354 /** urls (attachments from Peertube) **/
355 p = xs_dict_get(msg, "url");
356
357 if (xs_type(p) == XSTYPE_LIST) {
358 const char *href = NULL;
359 const char *type = NULL;
360 int c = 0;
361 const xs_val *v;
362
363 while (href == NULL && xs_list_next(p, &v, &c)) {
364 if (xs_type(v) == XSTYPE_DICT) {
365 const char *mtype = xs_dict_get(v, "type");
366
367 if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "Link") == 0) {
368 mtype = xs_dict_get(v, "mediaType");
369 const xs_list *tag = xs_dict_get(v, "tag");
370
371 if (xs_type(mtype) == XSTYPE_STRING &&
372 strcmp(mtype, "application/x-mpegURL") == 0 &&
373 xs_type(tag) == XSTYPE_LIST) {
374 /* now iterate the tag list, looking for a video URL */
375 const xs_dict *d;
376 int c = 0;
377
378 while (href == NULL && xs_list_next(tag, &d, &c)) {
379 if (xs_type(d) == XSTYPE_DICT) {
380 if (xs_type(mtype = xs_dict_get(d, "mediaType")) == XSTYPE_STRING &&
381 xs_startswith(mtype, "video/")) {
382 const char *h = xs_dict_get(d, "href");
383
384 /* this is probably it */
385 if (xs_type(h) == XSTYPE_STRING) {
386 href = h;
387 type = mtype;
388 }
389 }
390 }
391 }
392 }
393 }
394 }
395 }
396
397 if (href && type) {
398 xs *d = xs_dict_new();
399 d = xs_dict_append(d, "href", href);
400 d = xs_dict_append(d, "type", type);
401 d = xs_dict_append(d, "name", "---");
402
403 l = xs_list_append(l, d);
404 }
405 }
406
407 return l;
408}
409
410
411int hashtag_in_msg(const xs_list *hashtags, const xs_dict *msg)
412/* returns 1 if the message contains any of the list of hashtags */
413{
414 if (xs_is_list(hashtags) && xs_is_dict(msg)) {
415 const xs_list *tags_in_msg = xs_dict_get(msg, "tag");
416
417 if (xs_is_list(tags_in_msg)) {
418 const xs_dict *te;
419
420 /* iterate the tags in the message */
421 xs_list_foreach(tags_in_msg, te) {
422 if (xs_is_dict(te)) {
423 const char *type = xs_dict_get(te, "type");
424 const char *name = xs_dict_get(te, "name");
425
426 if (xs_is_string(type) && xs_is_string(name)) {
427 if (strcmp(type, "Hashtag") == 0) {
428 xs *lc_name = xs_utf8_tolower(name);
429
430 if (xs_list_in(hashtags, lc_name) != -1)
431 return 1;
432 }
433 }
434 }
435 }
436 }
437 }
438
439 return 0;
440}
441
442
443int followed_hashtag_check(snac *user, const xs_dict *msg)
444/* returns 1 if this message contains a hashtag followed by me */
445{
446 return hashtag_in_msg(xs_dict_get(user->config, "followed_hashtags"), msg);
447}
448
449
450int blocked_hashtag_check(snac *user, const xs_dict *msg)
451/* returns 1 if this message contains a hashtag blocked by me */
452{
453 return hashtag_in_msg(xs_dict_get(user->config, "blocked_hashtags"), msg);
454}
455
456
457int timeline_request(snac *snac, const char **id, xs_str **wrk, int level)
458/* ensures that an entry and its ancestors are in the timeline */
459{
460 int status = 0;
461
462 if (level < MAX_CONVERSATION_LEVELS && !xs_is_null(*id)) {
463 xs *msg = NULL;
464
465 /* from a blocked instance? discard and break */
466 if (is_instance_blocked(*id)) {
467 snac_debug(snac, 1, xs_fmt("timeline_request blocked instance %s", *id));
468 return status;
469 }
470
471 /* is the object already there? */
472 if (!valid_status((status = object_get(*id, &msg)))) {
473 /* no; download it */
474 status = activitypub_request(snac, *id, &msg);
475 }
476
477 if (valid_status(status)) {
478 const xs_dict *object = msg;
479 const char *type = xs_dict_get(object, "type");
480
481 /* get the id again from the object, as it may be different */
482 const char *nid = xs_dict_get(object, "id");
483
484 if (xs_type(nid) != XSTYPE_STRING)
485 return 0;
486
487 if (wrk && strcmp(nid, *id) != 0) {
488 snac_debug(snac, 1,
489 xs_fmt("timeline_request canonical id for %s is %s", *id, nid));
490
491 *wrk = xs_dup(nid);
492 *id = *wrk;
493 }
494
495 if (xs_is_null(type))
496 type = "(null)";
497
498 srv_debug(2, xs_fmt("timeline_request type %s '%s'", nid, type));
499
500 if (strcmp(type, "Create") == 0) {
501 /* some software like lemmy nest Announce + Create + Note */
502 if (!xs_is_null(object = xs_dict_get(object, "object"))) {
503 type = xs_dict_get(object, "type");
504 nid = xs_dict_get(object, "id");
505 }
506 else
507 type = "(null)";
508 }
509
510 if (xs_match(type, POSTLIKE_OBJECT_TYPE)) {
511 if (content_match("filter_reject.txt", object))
512 snac_log(snac, xs_fmt("timeline_request rejected by content %s", nid));
513 else
514 if (blocked_hashtag_check(snac, object))
515 snac_log(snac, xs_fmt("timeline_request rejected by hashtag %s", nid));
516 else {
517 const char *actor = get_atto(object);
518
519 if (!xs_is_null(actor)) {
520 /* request (and drop) the actor for this entry */
521 if (!valid_status(actor_request(snac, actor, NULL))) {
522 /* failed? retry later */
523 enqueue_actor_refresh(snac, actor, 60);
524 }
525
526 /* does it have an ancestor? */
527 const char *in_reply_to = get_in_reply_to(object);
528
529 /* store */
530 timeline_add(snac, nid, object);
531
532 /* redistribute to lists for this user */
533 list_distribute(snac, actor, object);
534
535 /* recurse! */
536 timeline_request(snac, &in_reply_to, NULL, level + 1);
537 }
538 }
539 }
540 }
541 }
542
543 return status;
544}
545
546
547int send_to_inbox_raw(const char *keyid, const char *seckey,
548 const xs_str *inbox, const xs_dict *msg,
549 xs_val **payload, int *p_size, int timeout)
550/* sends a message to an Inbox */
551{
552 int status;
553 xs_dict *response;
554 xs *j_msg = xs_json_dumps((xs_dict *)msg, 4);
555
556 response = http_signed_request_raw(keyid, seckey, "POST", inbox,
557 NULL, j_msg, strlen(j_msg), &status, payload, p_size, timeout);
558
559 xs_free(response);
560
561 return status;
562}
563
564
565int send_to_inbox(snac *snac, const xs_str *inbox, const xs_dict *msg,
566 xs_val **payload, int *p_size, int timeout)
567/* sends a message to an Inbox */
568{
569 const char *seckey = xs_dict_get(snac->key, "secret");
570
571 return send_to_inbox_raw(snac->actor, seckey, inbox, msg, payload, p_size, timeout);
572}
573
574
575xs_str *get_actor_inbox(const char *actor, int shared)
576/* gets an actor's inbox */
577{
578 xs *data = NULL;
579 const char *v = NULL;
580
581 if (valid_status(actor_request(NULL, actor, &data))) {
582 /* try first endpoints/sharedInbox */
583 if (shared && (v = xs_dict_get(data, "endpoints")))
584 v = xs_dict_get(v, "sharedInbox");
585
586 /* try then the regular inbox */
587 if (xs_is_null(v))
588 v = xs_dict_get(data, "inbox");
589 }
590
591 return xs_is_null(v) ? NULL : xs_dup(v);
592}
593
594
595int send_to_actor(snac *snac, const char *actor, const xs_dict *msg,
596 xs_val **payload, int *p_size, int timeout)
597/* sends a message to an actor */
598{
599 int status = HTTP_STATUS_BAD_REQUEST;
600 xs *inbox = get_actor_inbox(actor, 1);
601
602 if (!xs_is_null(inbox))
603 status = send_to_inbox(snac, inbox, msg, payload, p_size, timeout);
604
605 return status;
606}
607
608
609void post_message(snac *snac, const char *actor, const xs_dict *msg)
610/* posts a message immediately (bypassing the output queues) */
611{
612 xs *payload = NULL;
613 int p_size;
614
615 int status = send_to_actor(snac, actor, msg, &payload, &p_size, 3);
616
617 srv_log(xs_fmt("post_message to actor %s %d", actor, status));
618
619 if (!valid_status(status))
620 /* cannot send right now, enqueue */
621 enqueue_message(snac, msg);
622}
623
624
625xs_list *recipient_list(snac *snac, const xs_dict *msg, int expand_public)
626/* returns the list of recipients for a message */
627{
628 const xs_val *to = xs_dict_get(msg, "to");
629 const xs_val *cc = xs_dict_get(msg, "cc");
630 xs_set rcpts;
631 int n;
632
633 xs_set_init(&rcpts);
634
635 const xs_list *lists[] = { to, cc, NULL };
636 for (n = 0; lists[n]; n++) {
637 xs_list *l = (xs_list *)lists[n];
638 const char *v;
639 xs *tl = NULL;
640
641 /* if it's a string, create a list with only one element */
642 if (xs_type(l) == XSTYPE_STRING) {
643 tl = xs_list_new();
644 tl = xs_list_append(tl, l);
645
646 l = tl;
647 }
648
649 while (xs_list_iter(&l, &v)) {
650 if (expand_public) {
651 xs *followers_url = xs_fmt("%s/followers", snac->actor);
652 if (strcmp(v, public_address) == 0 ||
653 /* check if it's a followers collection URL */
654 (xs_type(v) == XSTYPE_STRING &&
655 strcmp(v, followers_url) == 0) ||
656 (xs_type(v) == XSTYPE_LIST &&
657 xs_list_in(v, followers_url) != -1)) {
658 /* iterate the followers and add them */
659 xs *fwers = follower_list(snac);
660 const char *actor;
661
662 char *p = fwers;
663 while (xs_list_iter(&p, &actor))
664 xs_set_add(&rcpts, actor);
665 }
666 else
667 xs_set_add(&rcpts, v);
668 }
669 else
670 xs_set_add(&rcpts, v);
671 }
672 }
673
674 return xs_set_result(&rcpts);
675}
676
677
678int is_msg_public(const xs_dict *msg)
679/* checks if a message is public */
680{
681 const char *to = xs_dict_get(msg, "to");
682 const char *cc = xs_dict_get(msg, "cc");
683 int n;
684
685 const char *lists[] = { to, cc, NULL };
686 for (n = 0; lists[n]; n++) {
687 const xs_val *l = lists[n];
688
689 if (xs_type(l) == XSTYPE_STRING) {
690 if (strcmp(l, public_address) == 0)
691 return 1;
692 }
693 else
694 if (xs_type(l) == XSTYPE_LIST) {
695 if (xs_list_in(l, public_address) != -1)
696 return 1;
697 }
698 }
699
700 return 0;
701}
702
703
704int is_msg_from_private_user(const xs_dict *msg)
705/* checks if a message is from a local, private user */
706{
707 int ret = 0;
708
709 /* is this message from a local user? */
710 if (xs_startswith(xs_dict_get(msg, "id"), srv_baseurl)) {
711 const char *atto = get_atto(msg);
712 xs *l = xs_split(atto, "/");
713 const char *uid = xs_list_get(l, -1);
714 snac user;
715
716 if (uid && user_open(&user, uid)) {
717 if (xs_type(xs_dict_get(user.config, "private")) == XSTYPE_TRUE)
718 ret = 1;
719
720 user_free(&user);
721 }
722 }
723
724 return ret;
725}
726
727
728void followed_hashtag_distribute(const xs_dict *msg)
729/* distribute this post to all users following the included hashtags */
730{
731 const char *id = xs_dict_get(msg, "id");
732 const xs_list *tags_in_msg = xs_dict_get(msg, "tag");
733
734 if (!xs_is_string(id) || !xs_is_list(tags_in_msg) || xs_list_len(tags_in_msg) == 0)
735 return;
736
737 srv_debug(1, xs_fmt("followed_hashtag_distribute check for %s", id));
738
739 xs *users = user_list();
740 const char *uid;
741
742 xs_list_foreach(users, uid) {
743 snac user;
744
745 if (user_open(&user, uid)) {
746 if (followed_hashtag_check(&user, msg)) {
747 timeline_add(&user, id, msg);
748
749 snac_log(&user, xs_fmt("followed hashtag in %s", id));
750 }
751
752 user_free(&user);
753 }
754 }
755}
756
757
758int was_mentioned(snac *user, const xs_dict *msg)
759/* checks if user was mentioned here */
760{
761 const xs_list *l = xs_dict_get_def(msg, "tag", xs_stock(XSTYPE_LIST));
762 const xs_dict *d;
763
764 xs_list_foreach(l, d) {
765 const char *type = xs_dict_get_def(d, "type", "");
766 const char *href = xs_dict_get_def(d, "href", "");
767
768 if (strcmp(type, "Mention") == 0 && strcmp(href, user->actor) == 0)
769 return 1;
770 }
771
772 return 0;
773}
774
775
776int is_msg_for_me(snac *snac, const xs_dict *c_msg)
777/* checks if this message is for me */
778{
779 const char *type = xs_dict_get(c_msg, "type");
780 const char *actor = xs_dict_get(c_msg, "actor");
781
782 if (strcmp(actor, snac->actor) == 0) {
783 /* message by myself? (most probably via the shared-inbox) reject */
784 snac_debug(snac, 1, xs_fmt("ignoring message by myself"));
785 return 0;
786 }
787
788 if (xs_match(type, "Like|Announce|EmojiReact")) {
789 const char *object = xs_dict_get(c_msg, "object");
790 xs *obj = NULL;
791
792 if (xs_is_dict(object)) {
793 obj = xs_dup(object);
794 object = xs_dict_get(object, "id");
795 }
796
797 /* bad object id? reject */
798 if (!xs_is_string(object))
799 return 0;
800
801 /* try to get the object */
802 if (!xs_is_dict(obj))
803 object_get(object, &obj);
804
805 /* if it's about one of our posts, accept it */
806 if (is_msg_mine(snac, object))
807 return 2;
808
809 /* blocked by hashtag? */
810 if (blocked_hashtag_check(snac, obj))
811 return 0;
812
813 /* if it's by someone we follow, accept it */
814 if (following_check(snac, actor))
815 return 1;
816
817 /* do we follow any hashtag? */
818 if (followed_hashtag_check(snac, obj))
819 return 7;
820
821 return 0;
822 }
823
824 /* if it's an Undo, it must be from someone related to us */
825 if (xs_match(type, "Undo")) {
826 return follower_check(snac, actor) || following_check(snac, actor);
827 }
828
829 /* if it's an Accept + Follow, it must be for a Follow we created */
830 if (xs_match(type, "Accept")) {
831 return following_check(snac, actor);
832 }
833
834 /* if it's a Follow, it must be explicitly for us */
835 if (xs_match(type, "Follow")) {
836 const char *object = xs_dict_get(c_msg, "object");
837 return !xs_is_null(object) && (strcmp(snac->actor, object) == 0 || strcmp(snac->actor_alt, object) == 0);
838 }
839
840 /* only accept Ping directed to us */
841 if (xs_match(type, "Ping")) {
842 const char *dest = xs_dict_get(c_msg, "to");
843 return !xs_is_null(dest) && (strcmp(snac->actor, dest) == 0 || strcmp(snac->actor_alt, dest) == 0);
844 }
845
846 /* if it's not a Create or Update, allow as is */
847 if (!xs_match(type, "Create|Update")) {
848 return 1;
849 }
850
851 const xs_dict *msg = xs_dict_get(c_msg, "object");
852
853 /* any blocked hashtag? reject */
854 if (blocked_hashtag_check(snac, msg)) {
855 snac_debug(snac, 1, xs_fmt("blocked by hashtag %s", xs_dict_get(msg, "id")));
856 return 0;
857 }
858
859 int pub_msg = is_msg_public(c_msg);
860
861 /* if this message is public and we follow the actor of this post, allow */
862 if (pub_msg && following_check(snac, actor))
863 return 1;
864
865 xs *rcpts = recipient_list(snac, msg, 0);
866 xs_list *p = rcpts;
867 const xs_str *v;
868
869 xs *actor_followers = NULL;
870
871 if (!pub_msg) {
872 /* not a public message; get the actor and its followers list */
873 xs *actor_obj = NULL;
874
875 if (valid_status(object_get(actor, &actor_obj))) {
876 const xs_val *fw = xs_dict_get(actor_obj, "followers");
877 if (fw)
878 actor_followers = xs_dup(fw);
879 }
880 }
881
882 while(xs_list_iter(&p, &v)) {
883 /* explicitly for me? accept */
884 if (strcmp(v, snac->actor) == 0 || strcmp(v, snac->actor_alt) == 0)
885 return 2;
886
887 if (pub_msg) {
888 /* a public message for someone we follow? (probably cc'ed) accept */
889 if (strcmp(v, public_address) != 0 && following_check(snac, v))
890 return 5;
891 }
892 else
893 if (actor_followers && strcmp(v, actor_followers) == 0) {
894 /* if this message is for this actor's followers, are we one of them? */
895 if (following_check(snac, actor))
896 return 6;
897 }
898 }
899
900 /* accept if it's by someone we follow */
901 const char *atto = get_atto(msg);
902
903 if (pub_msg && !xs_is_null(atto) && following_check(snac, atto))
904 return 3;
905
906 /* is this message a reply to another? */
907 const char *irt = get_in_reply_to(msg);
908 if (!xs_is_null(irt)) {
909 xs *r_msg = NULL;
910
911 /* try to get the replied message */
912 if (valid_status(object_get(irt, &r_msg))) {
913 atto = get_atto(r_msg);
914
915 /* accept if the replied message is from someone we follow */
916 if (pub_msg && !xs_is_null(atto) && following_check(snac, atto))
917 return 4;
918 }
919 }
920
921 /* does this message contain a tag we are following? */
922 if (pub_msg && followed_hashtag_check(snac, msg))
923 return 7;
924
925 return 0;
926}
927
928
929xs_str *process_tags(snac *snac, const char *content, xs_list **tag)
930/* parses mentions and tags from content */
931{
932 xs_str *nc = xs_str_new(NULL);
933 xs_list *tl = *tag;
934 xs *split;
935 xs_list *p;
936 const xs_val *v;
937 int n = 0;
938
939 /* create a default server for incomplete mentions */
940 xs *def_srv = NULL;
941
942 if (xs_list_len(tl)) {
943 /* if there are any mentions, get the server from
944 the first one, which is the inReplyTo author */
945 p = tl;
946 while (xs_list_iter(&p, &v)) {
947 const char *type = xs_dict_get(v, "type");
948 const char *name = xs_dict_get(v, "name");
949
950 if (type && name && strcmp(type, "Mention") == 0) {
951 xs *l = xs_split(name, "@");
952
953 def_srv = xs_dup(xs_list_get(l, -1));
954
955 break;
956 }
957 }
958 }
959
960 if (xs_is_null(def_srv))
961 /* use this same server */
962 def_srv = xs_dup(xs_dict_get(srv_config, "host"));
963
964 split = xs_regex_split(content, "(@[A-Za-z0-9_-]+(@[A-Za-z0-9\\.-]+)?|&#[0-9]+;|#(_|[^[:punct:][:space:]])+)");
965
966 p = split;
967 while (xs_list_iter(&p, &v)) {
968 if ((n & 0x1)) {
969 if (*v == '@') {
970 xs *link = NULL;
971 xs *wuid = NULL;
972
973 if (strchr(v + 1, '@') == NULL) {
974 /* only one @? it's a dumb Mastodon-like mention
975 without server; add the default one */
976 wuid = xs_fmt("%s@%s", v, def_srv);
977
978 snac_debug(snac, 2, xs_fmt("mention without server '%s' '%s'", v, wuid));
979 }
980 else
981 wuid = xs_dup(v);
982
983 /* query the webfinger about this fellow */
984 xs *actor = NULL;
985 xs *uid = NULL;
986 int status;
987
988 status = webfinger_request(wuid, &actor, &uid);
989
990 if (valid_status(status) && actor && uid) {
991 xs *d = xs_dict_new();
992 xs *n = xs_fmt("@%s", uid);
993
994 d = xs_dict_append(d, "type", "Mention");
995 d = xs_dict_append(d, "href", actor);
996 d = xs_dict_append(d, "name", n);
997
998 tl = xs_list_append(tl, d);
999
1000 link = xs_fmt("<span class=\"h-card\"><a href=\"%s\" class=\"u-url mention\">%s</a></span>", actor, n);
1001 }
1002
1003 if (!xs_is_null(link))
1004 nc = xs_str_cat(nc, link);
1005 else
1006 nc = xs_str_cat(nc, v);
1007 }
1008 else
1009 if (*v == '#') {
1010 /* hashtag */
1011 xs *d = xs_dict_new();
1012 xs *n = xs_utf8_tolower(v);
1013 xs *h = xs_fmt("%s?t=%s", srv_baseurl, n + 1);
1014 xs *l = xs_fmt("<a href=\"%s\" class=\"mention hashtag\" rel=\"tag\">%s</a>", h, v);
1015
1016 d = xs_dict_append(d, "type", "Hashtag");
1017 d = xs_dict_append(d, "href", h);
1018 d = xs_dict_append(d, "name", n);
1019
1020 tl = xs_list_append(tl, d);
1021
1022 /* add the code */
1023 nc = xs_str_cat(nc, l);
1024 }
1025 else
1026 if (*v == '&') {
1027 /* HTML Unicode entity, probably part of an emoji */
1028
1029 /* write as is */
1030 nc = xs_str_cat(nc, v);
1031 }
1032 }
1033 else
1034 nc = xs_str_cat(nc, v);
1035
1036 n++;
1037 }
1038
1039 *tag = tl;
1040
1041 return nc;
1042}
1043
1044
1045void collect_replies(snac *user, const char *id)
1046/* collects all replies for a post */
1047{
1048 xs *obj = NULL;
1049
1050 if (!valid_status(object_get(id, &obj))) {
1051 snac_debug(user, 1, xs_fmt("collect_replies: object '%s' is not here", id));
1052 return;
1053 }
1054
1055 const xs_dict *replies_first = xs_dict_get_path(obj, "replies.first");
1056 if (!xs_is_dict(replies_first)) {
1057 snac_debug(user, 1, xs_fmt("collect_replies: object '%s' does not have replies.first", id));
1058 return;
1059 }
1060
1061 const xs_list *level0_replies = xs_dict_get(replies_first, "items");
1062 const xs_list *level1_replies = NULL;
1063
1064 const char *next = xs_dict_get(replies_first, "next");
1065 xs *reply_obj = NULL;
1066
1067 if (xs_is_string(next) && !valid_status(activitypub_request(user, next, &reply_obj))) {
1068 snac_debug(user, 1, xs_fmt("collect_replies: error getting next replies object '%s'", next));
1069 return;
1070 }
1071
1072 if (xs_is_dict(reply_obj))
1073 level1_replies = xs_dict_get(reply_obj, "items");
1074
1075 xs *items = xs_list_new();
1076
1077 if (xs_is_list(level0_replies))
1078 items = xs_list_cat(items, level0_replies);
1079
1080 if (xs_is_list(level1_replies))
1081 items = xs_list_cat(items, level1_replies);
1082
1083 const xs_val *v;
1084
1085 xs_list_foreach(items, v) {
1086 xs *reply = NULL;
1087
1088 if (xs_is_string(v)) {
1089 /* request the object */
1090 if (!valid_status(object_get(v, &reply))) {
1091 if (!valid_status(activitypub_request(user, v, &reply))) {
1092 snac_debug(user, 1, xs_fmt("collect_replies: error requesting object '%s'", v));
1093 continue;
1094 }
1095 }
1096 }
1097 else
1098 if (xs_is_dict(v)) {
1099 /* actually the post object */
1100 reply = xs_dup(v);
1101 }
1102
1103 if (!xs_is_dict(reply))
1104 continue;
1105
1106 const char *id = xs_dict_get(reply, "id");
1107 const char *type = xs_dict_get(reply, "type");
1108 const char *attr_to = get_atto(reply);
1109
1110 if (!xs_is_string(id) || !xs_is_string(type) || !xs_is_string(attr_to))
1111 continue;
1112
1113 if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
1114 continue;
1115
1116 if (timeline_here(user, id)) {
1117 snac_debug(user, 1, xs_fmt("collect_replies: item already in timeline %s", id));
1118 continue;
1119 }
1120
1121 enqueue_actor_refresh(user, attr_to, 0);
1122
1123 timeline_add(user, id, reply);
1124
1125 snac_log(user, xs_fmt("new '%s' (collect_replies) %s %s", type, attr_to, id));
1126 }
1127}
1128
1129
1130void collect_outbox(snac *user, const char *actor_id)
1131/* gets an actor's outbox and inserts a bunch of posts in a user's timeline */
1132{
1133 int status;
1134 xs *actor = NULL;
1135
1136 if (!valid_status(status = actor_request(user, actor_id, &actor))) {
1137 snac_debug(user, 1, xs_fmt("collect_outbox: cannot get actor object '%s' %d", actor_id, status));
1138 return;
1139 }
1140
1141 xs *outbox = NULL;
1142 const char *outbox_url = xs_dict_get(actor, "outbox");
1143
1144 if (!xs_is_string(outbox_url))
1145 return;
1146
1147 if (!valid_status(status = activitypub_request(user, outbox_url, &outbox))) {
1148 snac_debug(user, 1, xs_fmt("collect_outbox: cannot get actor outbox '%s' %d", outbox_url, status));
1149 return;
1150 }
1151
1152 const xs_list *ordered_items = xs_dict_get(outbox, "orderedItems");
1153
1154 if (!xs_is_list(ordered_items)) {
1155 /* the list is not here; does it have a 'first'? */
1156 const char *first = xs_dict_get(outbox, "first");
1157
1158 if (xs_is_string(first)) {
1159 /* download this instead */
1160 xs *first2 = xs_dup(first);
1161 xs_free(outbox);
1162
1163 if (!valid_status(status = activitypub_request(user, first2, &outbox))) {
1164 snac_debug(user, 1, xs_fmt("collect_outbox: cannot get first page of outbox '%s' %d", first2, status));
1165 return;
1166 }
1167
1168 /* last chance */
1169 ordered_items = xs_dict_get(outbox, "orderedItems");
1170 }
1171 }
1172
1173 if (!xs_is_list(ordered_items)) {
1174 snac_debug(user, 1, xs_fmt("collect_outbox: cannot get list of posts for actor '%s' outbox", actor_id));
1175 return;
1176 }
1177
1178 /* well, ok, then */
1179 int max = 4;
1180 const xs_val *v;
1181
1182 xs_list_foreach(ordered_items, v) {
1183 if (max == 0)
1184 break;
1185
1186 xs *post = NULL;
1187
1188 if (xs_is_string(v)) {
1189 /* it's probably the post url */
1190 if (!valid_status(activitypub_request(user, v, &post)))
1191 continue;
1192 }
1193 else
1194 if (xs_is_dict(v))
1195 post = xs_dup(v);
1196
1197 if (post == NULL)
1198 continue;
1199
1200 const char *type = xs_dict_get(post, "type");
1201
1202 if (!xs_is_string(type) || strcmp(type, "Create")) {
1203 /* not a post */
1204 continue;
1205 }
1206 if (is_msg_for_me(user, post) == 0) {
1207 /* not for us */
1208 continue;
1209 }
1210
1211 const xs_dict *object = xs_dict_get(post, "object");
1212
1213 if (!xs_is_dict(object))
1214 continue;
1215
1216 type = xs_dict_get(object, "type");
1217 const char *id = xs_dict_get(object, "id");
1218 const char *attr_to = get_atto(object);
1219
1220 if (!xs_is_string(type) || !xs_is_string(id) || !xs_is_string(attr_to))
1221 continue;
1222
1223 if (!timeline_here(user, id)) {
1224 timeline_add(user, id, object);
1225 snac_log(user, xs_fmt("new '%s' (collect_outbox) %s %s", type, attr_to, id));
1226 }
1227 else
1228 snac_debug(user, 1, xs_fmt("collect_outbox: post '%s' already here", id));
1229
1230 max--;
1231 }
1232}
1233
1234
1235void notify(snac *snac, const char *type, const char *utype, const char *actor, const xs_dict *msg)
1236/* notifies the user of relevant events */
1237{
1238 /* skip our own notifications */
1239 if (strcmp(snac->actor, actor) == 0)
1240 return;
1241
1242 const char *id = xs_dict_get(msg, "id");
1243
1244 if (strcmp(type, "Create") == 0) {
1245 /* only notify of notes specifically for us */
1246 xs *rcpts = recipient_list(snac, msg, 0);
1247
1248 if (xs_list_in(rcpts, snac->actor) == -1 && !cool(snac, actor, OP_CHECK))
1249 return;
1250
1251 /* discard votes */
1252 const xs_dict *note = xs_dict_get(msg, "object");
1253
1254 if (note && !xs_is_null(xs_dict_get(note, "name")))
1255 return;
1256 }
1257
1258 if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") != 0)
1259 return;
1260
1261 /* get the object id */
1262 const char *objid = xs_dict_get(msg, "object");
1263
1264 if (xs_type(objid) == XSTYPE_DICT)
1265 objid = xs_dict_get(objid, "id");
1266
1267 if (xs_match(type, "Like|Announce|EmojiReact")) {
1268 /* if it's not an admiration about something by us, done */
1269 if (xs_is_null(objid) || !is_msg_mine(snac, objid))
1270 return;
1271
1272 /* if it's an announce by our own relay, done */
1273 xs *relay_id = xs_fmt("%s/relay", srv_baseurl);
1274 if (xs_startswith(id, relay_id))
1275 return;
1276 }
1277
1278 /* updated poll? */
1279 if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0) {
1280 const xs_dict *poll;
1281 const char *poll_id;
1282
1283 if ((poll = xs_dict_get(msg, "object")) == NULL)
1284 return;
1285
1286 /* if it's not closed, discard */
1287 if (xs_is_null(xs_dict_get(poll, "closed")))
1288 return;
1289
1290 if ((poll_id = xs_dict_get(poll, "id")) == NULL)
1291 return;
1292
1293 /* if it's not ours and we didn't vote, discard */
1294 if (!is_msg_mine(snac, poll_id) && !was_question_voted(snac, poll_id))
1295 return;
1296 }
1297
1298 /* user will love to know about this! */
1299
1300 /* prepare message body */
1301 xs *body = xs_fmt("User : @%s@%s\n",
1302 xs_dict_get(snac->config, "uid"),
1303 xs_dict_get(srv_config, "host")
1304 );
1305
1306 if (strcmp(utype, "(null)") != 0) {
1307 xs *s1 = xs_fmt("Type : %s + %s\n", type, utype);
1308 body = xs_str_cat(body, s1);
1309 }
1310 else {
1311 xs *s1 = xs_fmt("Type : %s\n", type);
1312 body = xs_str_cat(body, s1);
1313 }
1314
1315 {
1316 xs *s1 = xs_fmt("Actor : %s\n", actor);
1317 body = xs_str_cat(body, s1);
1318 }
1319
1320 if (objid != NULL) {
1321 xs *s1 = xs_fmt("Object: %s\n", objid);
1322 body = xs_str_cat(body, s1);
1323 }
1324
1325 /* email */
1326
1327 const char *email = "[disabled by admin]";
1328
1329 if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
1330 email = xs_dict_get(snac->config_o, "email");
1331 if (xs_is_null(email)) {
1332 email = xs_dict_get(snac->config, "email");
1333
1334 if (xs_is_null(email))
1335 email = "[empty]";
1336 }
1337 }
1338
1339 if (*email != '\0' && *email != '[') {
1340 snac_debug(snac, 1, xs_fmt("email notify %s %s %s", type, utype, actor));
1341
1342 xs *subject = xs_fmt("snac notify for @%s@%s",
1343 xs_dict_get(snac->config, "uid"), xs_dict_get(srv_config, "host"));
1344 xs *from = xs_fmt("<snac-daemon@%s>", xs_dict_get(srv_config, "host"));
1345 xs *header = xs_fmt(
1346 "From: snac-daemon %s\n"
1347 "To: %s\n"
1348 "Subject: %s\n"
1349 "\n",
1350 from, email, subject);
1351
1352 xs *mailinfo = xs_dict_new();
1353 xs *bd = xs_fmt("%s%s", header, body);
1354 mailinfo = xs_dict_append(mailinfo, "from", from);
1355 mailinfo = xs_dict_append(mailinfo, "to", email);
1356 mailinfo = xs_dict_append(mailinfo, "body", bd);
1357
1358 enqueue_email(mailinfo, 0);
1359 }
1360
1361 /* telegram */
1362
1363 const char *bot = xs_dict_get(snac->config, "telegram_bot");
1364 const char *chat_id = xs_dict_get(snac->config, "telegram_chat_id");
1365
1366 if (!xs_is_null(bot) && !xs_is_null(chat_id) && *bot && *chat_id)
1367 enqueue_telegram(body, bot, chat_id);
1368
1369 /* ntfy */
1370 const char *ntfy_server = xs_dict_get(snac->config, "ntfy_server");
1371 const char *ntfy_token = xs_dict_get(snac->config, "ntfy_token");
1372
1373 if (!xs_is_null(ntfy_server) && *ntfy_server)
1374 enqueue_ntfy(body, ntfy_server, ntfy_token);
1375
1376 /* auto boost */
1377 if (xs_match(type, "Create") && xs_is_true(xs_dict_get(snac->config, "auto_boost"))) {
1378 xs *msg = msg_admiration(snac, objid, "Announce");
1379 enqueue_message(snac, msg);
1380
1381 snac_debug(snac, 1, xs_fmt("auto boosted %s", objid));
1382 }
1383
1384 /* finally, store it in the notification folder */
1385 if (strcmp(type, "Follow") == 0)
1386 objid = id;
1387 else
1388 if (strcmp(utype, "Follow") == 0)
1389 objid = actor;
1390
1391 notify_add(snac, type, utype, actor, objid != NULL ? objid : id, msg);
1392}
1393
1394/** messages **/
1395
1396xs_dict *msg_base(snac *snac, const char *type, const char *id,
1397 const char *actor, const char *date, const char *object)
1398/* creates a base ActivityPub message */
1399{
1400 xs *did = NULL;
1401 xs *published = NULL;
1402 xs *ntid = tid(0);
1403 const char *obj_id;
1404
1405 if (xs_type(object) == XSTYPE_DICT)
1406 obj_id = xs_dict_get(object, "id");
1407 else
1408 obj_id = object;
1409
1410 /* generated values */
1411 if (date && strcmp(date, "@now") == 0) {
1412 published = xs_str_utctime(0, ISO_DATE_SPEC);
1413 date = published;
1414 }
1415
1416 if (id != NULL) {
1417 if (strcmp(id, "@dummy") == 0) {
1418 did = xs_fmt("%s/d/%s/%s", snac->actor, ntid, type);
1419
1420 id = did;
1421 }
1422 else
1423 if (strcmp(id, "@object") == 0) {
1424 if (obj_id != NULL) {
1425 did = xs_fmt("%s/%s_%s", obj_id, type, ntid);
1426 id = did;
1427 }
1428 else
1429 id = NULL;
1430 }
1431 else
1432 if (strcmp(id, "@wrapper") == 0) {
1433 /* like @object, but always generate the same id */
1434 if (object != NULL) {
1435 date = xs_dict_get(object, "published");
1436 did = xs_fmt("%s/%s", obj_id, type);
1437 id = did;
1438 }
1439 else
1440 id = NULL;
1441 }
1442 }
1443
1444 xs_dict *msg = xs_dict_new();
1445
1446 msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams");
1447 msg = xs_dict_append(msg, "type", type);
1448
1449 if (id != NULL)
1450 msg = xs_dict_append(msg, "id", id);
1451
1452 if (actor != NULL)
1453 msg = xs_dict_append(msg, "actor", actor);
1454
1455 if (date != NULL)
1456 msg = xs_dict_append(msg, "published", date);
1457
1458 if (object != NULL)
1459 msg = xs_dict_append(msg, "object", object);
1460
1461 return msg;
1462}
1463
1464
1465xs_dict *msg_collection(snac *snac, const char *id, int items)
1466/* creates an empty OrderedCollection message */
1467{
1468 xs_dict *msg = msg_base(snac, "OrderedCollection", id, NULL, NULL, NULL);
1469 xs *n = xs_number_new(items);
1470
1471 msg = xs_dict_append(msg, "attributedTo", snac->actor);
1472 msg = xs_dict_append(msg, "totalItems", n);
1473
1474 return msg;
1475}
1476
1477
1478xs_dict *msg_replies(snac *user, const char *id, int fill)
1479/* creates a CollectionPage with replies of id */
1480{
1481 xs *r_id = xs_replace(id, "/p/", "/r/");
1482 xs *r_idp = xs_fmt("%s?page=true", r_id);
1483 xs *r_idh = xs_fmt("%s", r_id);
1484
1485 xs_dict *msg = msg_base(user, "CollectionPage", r_idp, NULL, NULL, NULL);
1486
1487 msg = xs_dict_set(msg, "partOf", r_idh);
1488
1489 xs *items = xs_list_new();
1490 if (fill) {
1491 xs *children = object_children(id);
1492 const char *md5;
1493
1494 xs_list_foreach(children, md5) {
1495 xs *obj = NULL;
1496
1497 if (valid_status(object_get_by_md5(md5, &obj)) && is_msg_public(obj)) {
1498 const char *c_id = xs_dict_get(obj, "id");
1499
1500 if (xs_is_string(c_id))
1501 items = xs_list_append(items, c_id);
1502 }
1503 }
1504 }
1505 else {
1506 msg = xs_dict_del(msg, "@context");
1507 msg = xs_dict_del(msg, "id");
1508 msg = xs_dict_set(msg, "next", r_idp);
1509 }
1510
1511 msg = xs_dict_set(msg, "items", items);
1512
1513 return msg;
1514}
1515
1516
1517xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to)
1518/* creates an Accept message (as a response to a Follow) */
1519{
1520 xs_dict *msg = msg_base(snac, "Accept", "@dummy", snac->actor, NULL, object);
1521
1522 msg = xs_dict_append(msg, "to", to);
1523
1524 return msg;
1525}
1526
1527
1528xs_dict *msg_update(snac *snac, const xs_dict *object)
1529/* creates an Update message */
1530{
1531 xs_dict *msg = msg_base(snac, "Update", "@object", snac->actor, "@now", object);
1532
1533 const char *type = xs_dict_get(object, "type");
1534
1535 if (strcmp(type, "Note") == 0) {
1536 msg = xs_dict_append(msg, "to", xs_dict_get(object, "to"));
1537 msg = xs_dict_append(msg, "cc", xs_dict_get(object, "cc"));
1538 }
1539 else
1540 if (strcmp(type, "Person") == 0) {
1541 msg = xs_dict_append(msg, "to", public_address);
1542
1543 /* also spam the people being followed, so that
1544 they have the newest information about who we are */
1545 xs *cc = following_list(snac);
1546
1547 msg = xs_dict_append(msg, "cc", cc);
1548 }
1549 else
1550 msg = xs_dict_append(msg, "to", public_address);
1551
1552 return msg;
1553}
1554
1555
1556xs_dict *msg_admiration(snac *snac, const char *object, const char *type)
1557/* creates a Like, Announce or EmojiReact message */
1558{
1559 xs *a_msg = NULL;
1560 xs_dict *msg = NULL;
1561 xs *wrk = NULL;
1562 char t = 0;
1563
1564 switch (*type) {
1565 case 'L':
1566 t = 'l';
1567 break;
1568 case 'A':
1569 t = 'a';
1570 break;
1571 case 'E':
1572 t = 'e';
1573 break;
1574 }
1575
1576 /* call the object */
1577 timeline_request(snac, &object, &wrk, 0);
1578
1579 if (valid_status(object_get(object, &a_msg))) {
1580 xs *rcpts = xs_list_new();
1581 xs *o_md5 = xs_md5_hex(object, strlen(object));
1582 xs *id = xs_fmt("%s/%c/%s", snac->actor, t, o_md5);
1583
1584 msg = msg_base(snac, type, id, snac->actor, "@now", object);
1585
1586 if (is_msg_public(a_msg))
1587 rcpts = xs_list_append(rcpts, public_address);
1588
1589 rcpts = xs_list_append(rcpts, get_atto(a_msg));
1590
1591 msg = xs_dict_append(msg, "to", rcpts);
1592 }
1593 else
1594 snac_log(snac, xs_fmt("msg_admiration cannot retrieve object %s", object));
1595
1596 return msg;
1597}
1598
1599
1600xs_dict *msg_repulsion(snac *user, const char *id, const char *type)
1601/* creates an Undo + admiration message */
1602{
1603 xs *a_msg = NULL;
1604 xs_dict *msg = NULL;
1605
1606 if (valid_status(object_get(id, &a_msg))) {
1607 /* create a clone of the original admiration message */
1608 xs *object = msg_admiration(user, id, type);
1609
1610 /* delete the published date */
1611 object = xs_dict_del(object, "published");
1612
1613 /* create an undo message for this object */
1614 msg = msg_undo(user, object);
1615
1616 /* copy the 'to' field */
1617 msg = xs_dict_set(msg, "to", xs_dict_get(object, "to"));
1618 }
1619
1620 /* now we despise this */
1621 object_unadmire(id, user->actor, *type == 'L' ? 1 : 0);
1622
1623 return msg;
1624}
1625
1626xs_dict *msg_emoji_init(snac *snac, const char *mid, const char *eid_o)
1627/* creates an emoji reaction from a local user */
1628{
1629 xs_dict *n_msg = msg_admiration(snac, mid, "EmojiReact");
1630
1631 xs *eid = xs_dup(eid_o);
1632 xs *content = NULL;
1633 xs *tag = xs_list_new();
1634 xs *dict = xs_dict_new();
1635 xs *icon = xs_dict_new();
1636 xs *accounts = xs_list_new();
1637 xs *emjs = emojis_rm_categories();
1638
1639 /* may be a default emoji */
1640 xs *eidd = xs_dup(eid);
1641 const char *eidda = eid;
1642
1643 if (xs_is_emoji(xs_utf8_dec(&eidda)))
1644 content = xs_dup(eid);
1645
1646 else if (*eid == '%') {
1647 content = xs_url_dec_emoji(eid);
1648 if (content == NULL) {
1649 xs_free(n_msg);
1650 return NULL;
1651 }
1652 }
1653 else {
1654 content = xs_dup(eid);
1655 const char *emo = xs_dict_get(emjs, content);
1656
1657 if (emo == NULL) {
1658 xs_free(n_msg);
1659 return NULL;
1660 }
1661
1662 if (xs_match(emo, "https://*|http://*")) { /* emoji is an URL to an image */
1663 icon = xs_dict_set(icon, "type", "Image");
1664 icon = xs_dict_set(icon, "url", emo);
1665 dict = xs_dict_set(dict, "icon", icon);
1666
1667 dict = xs_dict_set(dict, "id", emo);
1668 dict = xs_dict_set(dict, "name", content);
1669 dict = xs_dict_set(dict, "type", "Emoji");
1670 tag = xs_list_append(tag, dict);
1671 }
1672 else
1673 if (xs_startswith(emo, "&#")) {
1674 /* snac default emoji as an HTML entity: decode */
1675 content = xs_free(content);
1676
1677 xs *s1 = xs_strip_chars_i(xs_dup(emo), "&#");
1678 unsigned int cpoint = 0;
1679 sscanf(s1, "%u;", &cpoint);
1680
1681 if (cpoint)
1682 content = xs_utf8_cat(xs_str_new(NULL), cpoint);
1683 else
1684 content = xs_dup(emo);
1685 }
1686 else {
1687 /* use as it is and hope for the best */
1688 xs_free(content);
1689 content = xs_dup(emo);
1690 }
1691 }
1692
1693 accounts = xs_list_append(accounts, snac->actor);
1694
1695 xs *to_duplicate = xs_dup(xs_list_get(xs_dict_get(n_msg, "to"), 1));
1696 n_msg = xs_dict_set(n_msg, "content", content);
1697 n_msg = xs_dict_set(n_msg, "accounts", accounts);
1698 n_msg = xs_dict_set(n_msg, "attributedTo", to_duplicate);
1699 n_msg = xs_dict_set(n_msg, "accountId", snac->uid);
1700 n_msg = xs_dict_set(n_msg, "tag", tag);
1701
1702 int ret = timeline_admire(snac, xs_dict_get(n_msg, "object"), snac->actor, 1, n_msg);
1703 if (ret == 200 || ret == 201) {
1704 enqueue_message(snac, n_msg);
1705 return n_msg;
1706 }
1707
1708 xs_free(n_msg);
1709 return NULL;
1710}
1711
1712xs_dict *msg_emoji_unreact(snac *user, const char *mid, const char *eid)
1713/* creates an Undo + emoji reaction message */
1714{
1715 xs *a_msg = NULL;
1716 xs_dict *msg = NULL;
1717
1718 if (valid_status(object_get(mid, &a_msg))) {
1719 /* create a clone of the original admiration message */
1720 xs *object = msg_admiration(user, mid, "EmojiReact");
1721
1722 /* delete the published date */
1723 object = xs_dict_del(object, "published");
1724
1725 /* create an undo message for this object */
1726 msg = msg_undo(user, object);
1727
1728 /* copy the 'to' field */
1729 msg = xs_dict_set(msg, "to", xs_dict_get(object, "to"));
1730 }
1731
1732 xs *emotes = object_get_emoji_reacts(mid);
1733 const char *v;
1734 int c = 0;
1735
1736 /* may be a default emoji */
1737 if (strlen(eid) == 12 && *eid == '%') {
1738 eid = xs_url_dec(eid);
1739 if (eid == NULL) {
1740 xs_free(msg);
1741 return NULL;
1742 }
1743 }
1744
1745 /* lets get all emotes for this msg, and compare it to our content */
1746 while (xs_list_next(emotes, &v, &c)) {
1747 xs *e = NULL;
1748 if (valid_status(object_get_by_md5(v, &e))) {
1749 const char *content = xs_dict_get(e, "content");
1750 const char *id = xs_dict_get(e, "id");
1751 const char *actor = xs_dict_get(e, "actor");
1752 /* maybe formated as :{emoteName}: too */
1753 if (xs_str_in(eid, content) != -1)
1754 if (strcmp(user->actor, actor) == 0) {
1755 object_rm_emoji_react(mid, id);
1756 return msg;
1757 }
1758 }
1759 }
1760
1761 xs_free(msg);
1762 return NULL;
1763}
1764
1765
1766xs_dict *msg_actor_place(snac *user, const char *label)
1767/* creates a Place object, if the user has a location defined */
1768{
1769 xs_dict *place = NULL;
1770 const char *latitude = xs_dict_get_def(user->config, "latitude", "");
1771 const char *longitude = xs_dict_get_def(user->config, "longitude", "");
1772
1773 if (*latitude && *longitude) {
1774 xs *d_la = xs_number_new(atof(latitude));
1775 xs *d_lo = xs_number_new(atof(longitude));
1776
1777 place = msg_base(user, "Place", NULL, user->actor, NULL, NULL);
1778
1779 place = xs_dict_set(place, "name", label);
1780 place = xs_dict_set(place, "latitude", d_la);
1781 place = xs_dict_set(place, "longitude", d_lo);
1782 }
1783
1784 return place;
1785}
1786
1787
1788xs_dict *msg_actor(snac *snac)
1789/* create a Person message for this actor */
1790{
1791 xs *ctxt = xs_list_new();
1792 xs *icon = xs_dict_new();
1793 xs *keys = xs_dict_new();
1794 xs *tags = xs_list_new();
1795 xs *avtr = NULL;
1796 xs *kid = NULL;
1797 xs *f_bio = NULL;
1798 xs_dict *msg = NULL;
1799 const char *p;
1800 int n;
1801
1802 /* everybody loves some caching */
1803 if (time(NULL) - object_mtime(snac->actor) < 3 * 3600 &&
1804 valid_status(object_get(snac->actor, &msg))) {
1805 snac_debug(snac, 2, xs_fmt("Returning cached actor %s", snac->actor));
1806
1807 return msg;
1808 }
1809
1810 msg = msg_base(snac, "Person", snac->actor, NULL, NULL, NULL);
1811
1812 /* change the @context (is this really necessary?) */
1813 ctxt = xs_list_append(ctxt, "https:/" "/www.w3.org/ns/activitystreams");
1814 ctxt = xs_list_append(ctxt, "https:/" "/w3id.org/security/v1");
1815 msg = xs_dict_set(msg, "@context", ctxt);
1816
1817 msg = xs_dict_set(msg, "url", snac->actor);
1818 msg = xs_dict_set(msg, "name", xs_dict_get(snac->config, "name"));
1819 msg = xs_dict_set(msg, "preferredUsername", snac->uid);
1820 msg = xs_dict_set(msg, "published", xs_dict_get(snac->config, "published"));
1821
1822 // this exists so we get the emoji tags from our name too.
1823 // and then we just throw away the result, because it's kinda useless to have markdown in the display name.
1824 xs *name_dummy = not_really_markdown(xs_dict_get(snac->config, "name"), NULL, &tags);
1825
1826 xs *f_bio_2 = not_really_markdown(xs_dict_get(snac->config, "bio"), NULL, &tags);
1827 f_bio = process_tags(snac, f_bio_2, &tags);
1828 msg = xs_dict_set(msg, "summary", f_bio);
1829 msg = xs_dict_set(msg, "tag", tags);
1830
1831 char *folders[] = { "inbox", "outbox", "followers", "following", "featured", NULL };
1832 for (n = 0; folders[n]; n++) {
1833 xs *f = xs_fmt("%s/%s", snac->actor, folders[n]);
1834 msg = xs_dict_set(msg, folders[n], f);
1835 }
1836
1837 p = xs_dict_get(snac->config, "avatar");
1838
1839 if (*p == '\0')
1840 avtr = xs_fmt("%s/susie.png", srv_baseurl);
1841 else
1842 avtr = xs_dup(p);
1843
1844 icon = xs_dict_append(icon, "type", "Image");
1845 icon = xs_dict_append(icon, "mediaType", xs_mime_by_ext(avtr));
1846 icon = xs_dict_append(icon, "url", avtr);
1847 msg = xs_dict_set(msg, "icon", icon);
1848
1849 kid = xs_fmt("%s#main-key", snac->actor);
1850
1851 keys = xs_dict_append(keys, "id", kid);
1852 keys = xs_dict_append(keys, "owner", snac->actor);
1853
1854 xs *public_key_pem = xs_dup(xs_dict_get(snac->key, "public"));
1855
1856 if (xs_is_string(public_key_pem)) {
1857 /* crop any garbage after the PEM */
1858 const char *pem_trailer = "\n-----END PUBLIC KEY-----\n";
1859 int i = xs_str_in(public_key_pem, pem_trailer);
1860 if (i > 0)
1861 public_key_pem[i + strlen(pem_trailer)] = '\0';
1862 }
1863
1864 keys = xs_dict_append(keys, "publicKeyPem", public_key_pem);
1865 msg = xs_dict_set(msg, "publicKey", keys);
1866
1867 /* if the "bot" config field is set to true, change type to "Service" */
1868 if (xs_type(xs_dict_get(snac->config, "bot")) == XSTYPE_TRUE)
1869 msg = xs_dict_set(msg, "type", "Service");
1870
1871 /* if it's named "relay", then identify as an "Application" */
1872 if (strcmp(snac->uid, "relay") == 0)
1873 msg = xs_dict_set(msg, "type", "Application");
1874
1875 /* add the header image, if there is one defined */
1876 const char *header = xs_dict_get(snac->config, "header");
1877 if (!xs_is_null(header)) {
1878 xs *d = xs_dict_new();
1879 d = xs_dict_append(d, "type", "Image");
1880 d = xs_dict_append(d, "mediaType", xs_mime_by_ext(header));
1881 d = xs_dict_append(d, "url", header);
1882 msg = xs_dict_set(msg, "image", d);
1883 }
1884
1885 /* add the metadata as attachments of PropertyValue */
1886 xs *metadata = NULL;
1887 const xs_dict *md = xs_dict_get(snac->config, "metadata");
1888
1889 if (xs_type(md) == XSTYPE_DICT)
1890 metadata = xs_dup(md);
1891 else
1892 if (xs_type(md) == XSTYPE_STRING) {
1893 metadata = xs_dict_new();
1894 xs *l = xs_split(md, "\n");
1895 const char *ll;
1896
1897 xs_list_foreach(l, ll) {
1898 xs *kv = xs_split_n(ll, "=", 1);
1899 const char *k = xs_list_get(kv, 0);
1900 const char *v = xs_list_get(kv, 1);
1901
1902 if (k && v) {
1903 xs *kk = xs_strip_i(xs_dup(k));
1904 xs *vv = xs_strip_i(xs_dup(v));
1905 metadata = xs_dict_set(metadata, kk, vv);
1906 }
1907 }
1908 }
1909
1910 if (xs_type(metadata) == XSTYPE_DICT) {
1911 xs *attach = xs_list_new();
1912 const xs_str *k;
1913 const xs_str *v;
1914
1915 int c = 0;
1916 while (xs_dict_next(metadata, &k, &v, &c)) {
1917 xs *d = xs_dict_new();
1918
1919 xs *k2 = encode_html(k);
1920 xs *v2 = NULL;
1921
1922 if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/")) {
1923 xs *t = encode_html(v);
1924 v2 = xs_fmt("<a href=\"%s\" rel=\"me\">%s</a>", t, t);
1925 }
1926 else
1927 v2 = encode_html(v);
1928
1929 d = xs_dict_append(d, "type", "PropertyValue");
1930 d = xs_dict_append(d, "name", k2);
1931 d = xs_dict_append(d, "value", v2);
1932
1933 attach = xs_list_append(attach, d);
1934 }
1935
1936 msg = xs_dict_set(msg, "attachment", attach);
1937 }
1938
1939 /* use shared inboxes? */
1940 if (xs_is_true(xs_dict_get(srv_config, "shared_inboxes")) || strcmp(snac->uid, "relay") == 0) {
1941 xs *d = xs_dict_new();
1942 xs *si = xs_fmt("%s/shared-inbox", srv_baseurl);
1943 d = xs_dict_append(d, "sharedInbox", si);
1944 msg = xs_dict_set(msg, "endpoints", d);
1945 }
1946
1947 /* does this user have an aka? */
1948 const char *aka = xs_dict_get(snac->config, "alias");
1949 if (xs_type(aka) == XSTYPE_STRING && *aka) {
1950 xs *loaka = xs_list_append(xs_list_new(), aka);
1951
1952 msg = xs_dict_set(msg, "alsoKnownAs", loaka);
1953 }
1954
1955 const xs_val *manually = xs_dict_get(snac->config, "approve_followers");
1956 msg = xs_dict_set(msg, "manuallyApprovesFollowers",
1957 xs_stock(xs_is_true(manually) ? XSTYPE_TRUE : XSTYPE_FALSE));
1958
1959 /* if there are location coords, create a Place object */
1960 xs *location = msg_actor_place(snac, "Home");
1961 if (xs_type(location) == XSTYPE_DICT)
1962 msg = xs_dict_set(msg, "location", location);
1963
1964 xs *webfinger = xs_fmt("%s@%s", snac->uid, xs_dict_get(srv_config, "host"));
1965 msg = xs_dict_set(msg, "webfinger", webfinger);
1966
1967 if (xs_is_true(xs_dict_get(snac->config, "show_contact_metrics"))) {
1968 xs *fwing = xs_number_new_l(following_list_len(snac));
1969 xs *fwers = xs_number_new_l(follower_list_len(snac));
1970
1971 msg = xs_dict_set(msg, "following_count", fwing);
1972 msg = xs_dict_set(msg, "followers_count", fwers);
1973 }
1974
1975 /* cache it */
1976 snac_debug(snac, 1, xs_fmt("Caching actor %s", snac->actor));
1977 object_add_ow(snac->actor, msg);
1978
1979 return msg;
1980}
1981
1982
1983xs_dict *msg_create(snac *snac, const xs_dict *object)
1984/* creates a 'Create' message */
1985{
1986 xs_dict *msg = msg_base(snac, "Create", "@wrapper", snac->actor, NULL, object);
1987 const xs_val *v;
1988
1989 if ((v = get_atto(object)))
1990 msg = xs_dict_append(msg, "attributedTo", v);
1991
1992 if ((v = xs_dict_get(object, "cc")))
1993 msg = xs_dict_append(msg, "cc", v);
1994
1995 if ((v = xs_dict_get(object, "to")))
1996 msg = xs_dict_append(msg, "to", v);
1997 else
1998 msg = xs_dict_append(msg, "to", public_address);
1999
2000 return msg;
2001}
2002
2003
2004xs_dict *msg_undo(snac *snac, const xs_val *object)
2005/* creates an 'Undo' message */
2006{
2007 xs_dict *msg = msg_base(snac, "Undo", "@object", snac->actor, "@now", object);
2008 const char *to;
2009
2010 if (xs_type(object) == XSTYPE_DICT && (to = xs_dict_get(object, "object")))
2011 msg = xs_dict_append(msg, "to", to);
2012
2013 return msg;
2014}
2015
2016
2017xs_dict *msg_delete(snac *snac, const char *id)
2018/* creates a 'Delete' + 'Tombstone' for a local entry */
2019{
2020 xs *tomb = xs_dict_new();
2021 xs_dict *msg = NULL;
2022
2023 /* sculpt the tombstone */
2024 tomb = xs_dict_append(tomb, "type", "Tombstone");
2025 tomb = xs_dict_append(tomb, "id", id);
2026
2027 /* now create the Delete */
2028 msg = msg_base(snac, "Delete", "@object", snac->actor, "@now", tomb);
2029
2030 xs *to = xs_list_new();
2031 xs *involved = object_likes(id);
2032 xs *boosts = object_announces(id);
2033 xs *children = object_children(id);
2034 const char *md5;
2035
2036 involved = xs_list_cat(involved, boosts);
2037 involved = xs_list_cat(involved, children);
2038
2039 /* add everybody */
2040 to = xs_list_append(to, public_address);
2041
2042 /* add actors that liked, boosted or replied to this */
2043 xs_list_foreach(involved, md5) {
2044 xs *actor = NULL;
2045
2046 if (valid_status(object_get_by_md5(md5, &actor))) {
2047 const char *id = xs_dict_get(actor, "id");
2048 const char *atto = get_atto(actor);
2049
2050 if (xs_is_string(atto))
2051 to = xs_list_append(to, atto);
2052 else
2053 if (xs_is_string(id))
2054 to = xs_list_append(to, id);
2055 }
2056 }
2057
2058 msg = xs_dict_append(msg, "to", to);
2059
2060 return msg;
2061}
2062
2063
2064xs_dict *msg_follow(snac *snac, const char *q)
2065/* creates a 'Follow' message */
2066{
2067 xs *actor_o = NULL;
2068 xs *actor = NULL;
2069 xs_dict *msg = NULL;
2070 int status;
2071
2072 xs *url_or_uid = xs_strip_i(xs_str_new(q));
2073
2074 if (xs_startswith(url_or_uid, "https:/") || xs_startswith(url_or_uid, "http:/"))
2075 actor = xs_dup(url_or_uid);
2076 else
2077 if (!valid_status(webfinger_request(url_or_uid, &actor, NULL)) || actor == NULL) {
2078 snac_log(snac, xs_fmt("cannot resolve user %s to follow", url_or_uid));
2079 return NULL;
2080 }
2081
2082 /* request the actor */
2083 status = actor_request(snac, actor, &actor_o);
2084
2085 if (valid_status(status)) {
2086 /* check if the actor is an alias */
2087 const char *r_actor = xs_dict_get(actor_o, "id");
2088
2089 if (r_actor && strcmp(actor, r_actor) != 0) {
2090 snac_log(snac, xs_fmt("actor to follow is an alias %s -> %s", actor, r_actor));
2091 }
2092
2093 msg = msg_base(snac, "Follow", "@dummy", snac->actor, NULL, r_actor);
2094 }
2095 else
2096 snac_log(snac, xs_fmt("cannot get actor to follow %s %d", actor, status));
2097
2098 return msg;
2099}
2100
2101
2102xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
2103 const xs_str *in_reply_to, const xs_list *attach,
2104 int scope, const char *lang_str, const char *msg_date)
2105/* creates a 'Note' message */
2106/* scope: 0, public; 1, private (mentioned only); 2, "quiet public"; 3, followers only */
2107{
2108 xs *ntid = tid(0);
2109 xs *id = xs_fmt("%s/p/%s", snac->actor, ntid);
2110 xs *ctxt = NULL;
2111 xs *fc2 = NULL;
2112 xs *fc1 = NULL;
2113 xs *to = NULL;
2114 xs *cc = xs_list_new();
2115 xs *irt = NULL;
2116 xs *tag = xs_list_new();
2117 xs *atls = xs_list_new();
2118
2119 /* discard non-parseable dates */
2120 if (!xs_is_string(msg_date) || xs_parse_iso_date(msg_date, 0) == 0)
2121 msg_date = NULL;
2122
2123 xs_dict *msg = msg_base(snac, "Note", id, NULL, xs_or(msg_date, "@now"), NULL);
2124 xs_list *p;
2125 const xs_val *v;
2126
2127 const int priv = (scope == SCOPE_MENTIONED || scope == SCOPE_FOLLOWERS);
2128
2129 if (rcpts == NULL)
2130 to = xs_list_new();
2131 else {
2132 if (xs_type(rcpts) == XSTYPE_STRING) {
2133 to = xs_list_new();
2134 to = xs_list_append(to, rcpts);
2135 }
2136 else
2137 to = xs_dup(rcpts);
2138 }
2139
2140 /* format the content */
2141 fc2 = not_really_markdown(content, &atls, &tag);
2142
2143 if (in_reply_to != NULL && *in_reply_to) {
2144 xs *p_msg = NULL;
2145 xs *wrk = NULL;
2146
2147 /* demand this thing */
2148 timeline_request(snac, &in_reply_to, &wrk, 0);
2149
2150 if (valid_status(object_get(in_reply_to, &p_msg))) {
2151 /* add this author as recipient */
2152 const char *a, *v;
2153
2154 if ((a = get_atto(p_msg)) && xs_list_in(to, a) == -1)
2155 to = xs_list_append(to, a);
2156
2157 /* add this author to the tag list as a mention */
2158 if (!xs_is_null(a)) {
2159 xs *l = xs_split(a, "/");
2160 xs *actor_o = NULL;
2161
2162 if (xs_list_len(l) > 3 && valid_status(object_get(a, &actor_o))) {
2163 const char *uname = xs_dict_get(actor_o, "preferredUsername");
2164
2165 if (!xs_is_null(uname) && *uname) {
2166 xs *handle = xs_fmt("@%s@%s", uname, xs_list_get(l, 2));
2167
2168 xs *t = xs_dict_new();
2169
2170 t = xs_dict_append(t, "type", "Mention");
2171 t = xs_dict_append(t, "href", a);
2172 t = xs_dict_append(t, "name", handle);
2173
2174 tag = xs_list_append(tag, t);
2175 }
2176 }
2177 }
2178
2179 /* get the context, if there is one */
2180 if ((v = xs_dict_get(p_msg, "context")))
2181 ctxt = xs_dup(v);
2182
2183 /* propagate the conversation field, if there is one */
2184 if ((v = xs_dict_get(p_msg, "conversation")))
2185 msg = xs_dict_append(msg, "conversation", v);
2186
2187 /* if this message is public or unlisted, ours will also be */
2188 const int orig_scope = get_msg_visibility(p_msg);
2189 if (!priv && orig_scope == SCOPE_PUBLIC && xs_list_in(to, public_address) == -1)
2190 to = xs_list_append(to, public_address);
2191 else
2192 if (!priv && orig_scope == SCOPE_UNLISTED && xs_list_in(cc, public_address) == -1)
2193 cc = xs_list_append(cc, public_address);
2194 }
2195
2196 irt = xs_dup(in_reply_to);
2197 }
2198 else
2199 irt = xs_val_new(XSTYPE_NULL);
2200
2201 /* extract the mentions and hashtags and convert the content */
2202 fc1 = process_tags(snac, fc2, &tag);
2203
2204 /* create the attachment list, if there are any */
2205 if (!xs_is_null(attach)) {
2206 xs_list_foreach(attach, v) {
2207 const char *url = xs_list_get(v, 0);
2208 const char *alt = xs_list_get(v, 1);
2209 const char *mime = xs_mime_by_ext(url);
2210 int add = 1;
2211
2212 /* check if it's already here */
2213 const xs_dict *ad;
2214 xs_list_foreach(atls, ad) {
2215 if (strcmp(xs_dict_get_def(ad, "url", ""), url) == 0) {
2216 add = 0;
2217 break;
2218 }
2219 }
2220
2221 if (add) {
2222 xs *d = xs_dict_new();
2223 d = xs_dict_append(d, "mediaType", mime);
2224 d = xs_dict_append(d, "url", url);
2225 d = xs_dict_append(d, "name", alt);
2226 d = xs_dict_append(d, "type",
2227 xs_startswith(mime, "image/") ? "Image" : "Document");
2228
2229 atls = xs_list_append(atls, d);
2230 }
2231 }
2232 }
2233
2234 if (ctxt == NULL)
2235 ctxt = xs_fmt("%s#ctxt", id);
2236
2237 /* add all mentions to the appropriate field */
2238 p = tag;
2239 while (xs_list_iter(&p, &v)) {
2240 if (xs_type(v) == XSTYPE_DICT) {
2241 const char *t;
2242
2243 if (!xs_is_null(t = xs_dict_get(v, "type")) && strcmp(t, "Mention") == 0) {
2244 if (!xs_is_null(t = xs_dict_get(v, "href"))) {
2245 if (scope == SCOPE_MENTIONED) {
2246 /* for DMs, mentions go to 'to' */
2247 to = xs_list_append(to, t);
2248 } else {
2249 /* for other visibility levels, mentions go to 'cc' */
2250 cc = xs_list_append(cc, t);
2251 }
2252 }
2253 }
2254 }
2255 }
2256
2257 if (scope == SCOPE_UNLISTED) {
2258 /* Mastodon's "quiet public": remove from to, add public address to cc */
2259 int idx;
2260 if ((idx = xs_list_in(to, public_address)) != -1)
2261 to = xs_list_del(to, idx);
2262 if (xs_list_in(cc, public_address) == -1)
2263 cc = xs_list_append(cc, public_address);
2264 }
2265 else
2266 if (scope == SCOPE_FOLLOWERS) {
2267 /* followers-only: add followers collection to to */
2268 xs *followers_url = xs_fmt("%s/followers", snac->actor);
2269 if (xs_list_in(to, followers_url) == -1)
2270 to = xs_list_append(to, followers_url);
2271 }
2272 else
2273 if (scope == SCOPE_PUBLIC) {
2274 /* public: ensure public address is in to and not in cc */
2275 int idx;
2276 if ((idx = xs_list_in(cc, public_address)) != -1)
2277 cc = xs_list_del(cc, idx);
2278 if (xs_list_in(to, public_address) == -1)
2279 to = xs_list_append(to, public_address);
2280 }
2281
2282 /* delete all cc recipients that also are in the to */
2283 p = to;
2284 while (xs_list_iter(&p, &v)) {
2285 int i;
2286
2287 if ((i = xs_list_in(cc, v)) != -1)
2288 cc = xs_list_del(cc, i);
2289 }
2290
2291 msg = xs_dict_append(msg, "attributedTo", snac->actor);
2292 msg = xs_dict_append(msg, "summary", "");
2293 msg = xs_dict_append(msg, "content", fc1);
2294 msg = xs_dict_append(msg, "context", ctxt);
2295 msg = xs_dict_append(msg, "url", id);
2296 msg = xs_dict_append(msg, "to", to);
2297 msg = xs_dict_append(msg, "cc", cc);
2298 msg = xs_dict_append(msg, "inReplyTo", irt);
2299 msg = xs_dict_append(msg, "tag", tag);
2300
2301 msg = xs_dict_append(msg, "sourceContent", content);
2302
2303 if (xs_list_len(atls))
2304 msg = xs_dict_append(msg, "attachment", atls);
2305
2306 /* set language content map */
2307 if (xs_type(lang_str) == XSTYPE_STRING) {
2308 /* split at the first _ */
2309 xs *l0 = xs_split(lang_str, "_");
2310 const char *lang = xs_list_get(l0, 0);
2311
2312 if (xs_type(lang) == XSTYPE_STRING && strlen(lang) == 2) {
2313 /* a valid ISO language id */
2314 xs *cmap = xs_dict_new();
2315 cmap = xs_dict_set(cmap, lang, xs_dict_get(msg, "content"));
2316 msg = xs_dict_set(msg, "contentMap", cmap);
2317 }
2318 }
2319
2320 if (!priv) {
2321 /* create the replies object */
2322 xs *replies = xs_dict_new();
2323 xs *r_id = xs_replace(id, "/p/", "/r/");
2324 xs *h_id = xs_fmt("%s", r_id);
2325 xs *rp = msg_replies(snac, id, 0);
2326
2327 replies = xs_dict_set(replies, "id", h_id);
2328 replies = xs_dict_set(replies, "type", "Collection");
2329 replies = xs_dict_set(replies, "first", rp);
2330
2331 msg = xs_dict_set(msg, "replies", replies);
2332 }
2333
2334 return msg;
2335}
2336
2337
2338xs_dict *msg_ping(snac *user, const char *rcpt)
2339/* creates a Ping message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
2340{
2341 xs_dict *msg = msg_base(user, "Ping", "@dummy", user->actor, NULL, NULL);
2342
2343 msg = xs_dict_append(msg, "to", rcpt);
2344
2345 return msg;
2346}
2347
2348
2349xs_dict *msg_pong(snac *user, const char *rcpt, const char *object)
2350/* creates a Pong message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
2351{
2352 xs_dict *msg = msg_base(user, "Pong", "@dummy", user->actor, NULL, object);
2353
2354 msg = xs_dict_append(msg, "to", rcpt);
2355
2356 return msg;
2357}
2358
2359
2360xs_dict *msg_move(snac *user, const char *new_account)
2361/* creates a Move message (to move the user to new_account) */
2362{
2363 xs_dict *msg = msg_base(user, "Move", "@dummy", user->actor, NULL, user->actor);
2364
2365 msg = xs_dict_append(msg, "target", new_account);
2366
2367 return msg;
2368}
2369
2370
2371xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
2372 const xs_list *opts, int multiple, int end_secs)
2373/* creates a Question message */
2374{
2375 xs_dict *msg = msg_note(user, content, NULL, NULL, attach, 0, NULL, NULL);
2376 const xs_number *max_options = xs_dict_get(srv_config, "max_poll_options");
2377 const xs_number *max_length = xs_dict_get(srv_config, "max_poll_option_length");
2378 xs_set seen;
2379
2380 size_t max_line = 60;
2381 int max = 8;
2382 if (xs_type(max_options) == XSTYPE_NUMBER)
2383 max = xs_number_get(max_options);
2384 if (xs_type(max_length) == XSTYPE_NUMBER)
2385 max_line = xs_number_get(max_length);
2386
2387 msg = xs_dict_set(msg, "type", "Question");
2388
2389 /* make it non-editable */
2390 msg = xs_dict_del(msg, "sourceContent");
2391
2392 xs *o = xs_list_new();
2393 xs_list *p = (xs_list *)opts;
2394 const xs_str *v;
2395 xs *replies = xs_json_loads("{\"type\":\"Collection\",\"totalItems\":0}");
2396
2397 xs_set_init(&seen);
2398
2399 while (max && xs_list_iter(&p, &v)) {
2400 if (*v) {
2401 xs *v2 = xs_dup(v);
2402 xs *d = xs_dict_new();
2403
2404 if (strlen(v2) > max_line) {
2405 v2[max_line] = '\0';
2406 v2 = xs_str_cat(v2, "...");
2407 }
2408
2409 if (xs_set_add(&seen, v2) == 1) {
2410 d = xs_dict_append(d, "type", "Note");
2411 d = xs_dict_append(d, "name", v2);
2412 d = xs_dict_append(d, "replies", replies);
2413 o = xs_list_append(o, d);
2414
2415 max--;
2416 }
2417 }
2418 }
2419
2420 xs_set_free(&seen);
2421
2422 msg = xs_dict_append(msg, multiple ? "anyOf" : "oneOf", o);
2423
2424 /* set the end time */
2425 time_t t = time(NULL) + end_secs;
2426 xs *et = xs_str_utctime(t, ISO_DATE_SPEC);
2427
2428 msg = xs_dict_append(msg, "endTime", et);
2429
2430 return msg;
2431}
2432
2433int get_msg_visibility(const xs_dict *msg)
2434/* determine visibility from message based on CC, TO and /followers mark */
2435{
2436 const xs_val *to = xs_dict_get(msg, "to");
2437 const xs_val *cc = xs_dict_get(msg, "cc");
2438
2439 /* check if it's unlisted (public in cc but not in to) */
2440 int pub_in_to = 0;
2441 int pub_in_cc = 0;
2442
2443 if ((xs_type(to) == XSTYPE_STRING && strcmp(to, public_address) == 0) ||
2444 (xs_type(to) == XSTYPE_LIST && xs_list_in(to, public_address) != -1))
2445 pub_in_to = 1;
2446
2447 if ((xs_type(cc) == XSTYPE_STRING && strcmp(cc, public_address) == 0) ||
2448 (xs_type(cc) == XSTYPE_LIST && xs_list_in(cc, public_address) != -1))
2449 pub_in_cc = 1;
2450
2451 if (!pub_in_to && pub_in_cc) {
2452 return SCOPE_UNLISTED;
2453 }
2454 if (pub_in_to && !pub_in_cc) {
2455 return SCOPE_PUBLIC;
2456 }
2457
2458 xs *followers_url = xs_fmt("%s/followers", xs_dict_get(msg, "attributedTo"));
2459
2460 if ((xs_type(to) == XSTYPE_STRING && strcmp(to, followers_url) == 0) ||
2461 (xs_type(to) == XSTYPE_LIST && xs_list_in(to, followers_url) != -1))
2462 return SCOPE_FOLLOWERS;
2463 else
2464 return SCOPE_MENTIONED;
2465 /* should be unreachable, someone violated the protocol. */
2466 /* better treat it as followers-only to make sure we don't leak information.*/
2467 return SCOPE_FOLLOWERS;
2468}
2469
2470int update_question(snac *user, const char *id)
2471/* updates the poll counts */
2472{
2473 xs *msg = NULL;
2474 xs *rcnt = xs_dict_new();
2475 xs *lopts = xs_list_new();
2476 const xs_list *opts;
2477 xs_list *p;
2478 const xs_val *v;
2479
2480 /* get the object */
2481 if (!valid_status(object_get(id, &msg)))
2482 return -1;
2483
2484 /* closed? do nothing more */
2485 if (xs_dict_get(msg, "closed"))
2486 return -2;
2487
2488 /* get the options */
2489 if ((opts = xs_dict_get(msg, "oneOf")) == NULL &&
2490 (opts = xs_dict_get(msg, "anyOf")) == NULL)
2491 return -3;
2492
2493 /* fill the initial count */
2494 int c = 0;
2495 while (xs_list_next(opts, &v, &c)) {
2496 const char *name = xs_dict_get(v, "name");
2497 if (name) {
2498 lopts = xs_list_append(lopts, name);
2499 rcnt = xs_dict_set(rcnt, name, xs_stock(0));
2500 }
2501 }
2502
2503 xs_set s;
2504 xs_set_init(&s);
2505
2506 /* iterate now the children (the votes) */
2507 xs *chld = object_children(id);
2508 p = chld;
2509 while (xs_list_iter(&p, &v)) {
2510 xs *obj = NULL;
2511
2512 if (!valid_status(object_get_by_md5(v, &obj)))
2513 continue;
2514
2515 const char *name = xs_dict_get(obj, "name");
2516 const char *atto = get_atto(obj);
2517
2518 if (name && atto) {
2519 /* get the current count */
2520 const xs_number *cnt = xs_dict_get(rcnt, name);
2521
2522 if (xs_type(cnt) == XSTYPE_NUMBER) {
2523 /* if it exists, increment */
2524 xs *ucnt = xs_number_new(xs_number_get(cnt) + 1);
2525 rcnt = xs_dict_set(rcnt, name, ucnt);
2526
2527 xs_set_add(&s, atto);
2528 }
2529 }
2530 }
2531
2532 xs *rcpts = xs_set_result(&s);
2533
2534 /* create a new list of options with their new counts */
2535 xs *nopts = xs_list_new();
2536 p = lopts;
2537 while (xs_list_iter(&p, &v)) {
2538 const xs_number *cnt = xs_dict_get(rcnt, v);
2539
2540 if (xs_type(cnt) == XSTYPE_NUMBER) {
2541 xs *d1 = xs_dict_new();
2542 xs *d2 = xs_dict_new();
2543
2544 d2 = xs_dict_append(d2, "type", "Collection");
2545 d2 = xs_dict_append(d2, "totalItems", cnt);
2546
2547 d1 = xs_dict_append(d1, "type", "Note");
2548 d1 = xs_dict_append(d1, "name", v);
2549 d1 = xs_dict_append(d1, "replies", d2);
2550
2551 nopts = xs_list_append(nopts, d1);
2552 }
2553 }
2554
2555 /* update the list */
2556 msg = xs_dict_set(msg, xs_dict_get(msg, "oneOf") != NULL ? "oneOf" : "anyOf", nopts);
2557
2558 /* due date? */
2559 int closed = 0;
2560 const char *end_time = xs_dict_get(msg, "endTime");
2561 if (!xs_is_null(end_time)) {
2562 xs *now = xs_str_utctime(0, ISO_DATE_SPEC);
2563
2564 /* is now greater than the endTime? */
2565 if (strcmp(now, end_time) >= 0) {
2566 xs *et = xs_dup(end_time);
2567 msg = xs_dict_set(msg, "closed", et);
2568
2569 closed = 1;
2570 }
2571 }
2572
2573 /* update the count of voters */
2574 xs *vcnt = xs_number_new(xs_list_len(rcpts));
2575 msg = xs_dict_set(msg, "votersCount", vcnt);
2576 msg = xs_dict_set(msg, "cc", rcpts);
2577
2578 /* store */
2579 object_add_ow(id, msg);
2580
2581 snac_debug(user, 1, xs_fmt("recounted poll %s", id));
2582 timeline_touch(user);
2583
2584 /* send an update message to all voters */
2585 xs *u_msg = msg_update(user, msg);
2586 u_msg = xs_dict_set(u_msg, "cc", rcpts);
2587
2588 enqueue_message(user, u_msg);
2589
2590 if (closed) {
2591 xs *c_msg = msg_update(user, msg);
2592 notify(user, "Update", "Question", user->actor, c_msg);
2593 }
2594
2595 return 0;
2596}
2597
2598
2599/** queues **/
2600
2601int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2602/* processes an ActivityPub message from the input queue */
2603/* return values: -1, fatal error; 0, transient error, retry;
2604 1, processed and done; 2, propagate to users (only when no user is set) */
2605{
2606 const char *actor = xs_dict_get(msg, "actor");
2607 const char *type = xs_dict_get(msg, "type");
2608 xs *actor_o = NULL;
2609 int a_status;
2610 int do_notify = 0;
2611
2612 if (xs_is_null(actor) || *actor == '\0') {
2613 srv_debug(0, xs_fmt("malformed message (bad actor)"));
2614 return -1;
2615 }
2616
2617 /* this instance is alive */
2618 instance_failure(actor, OP_DEL);
2619
2620 /* question votes may not have a type */
2621 if (xs_is_null(type))
2622 type = "Note";
2623
2624 /* reject uninteresting messages right now */
2625 if (xs_match(type, "Add|View|Reject|Read|Remove")) {
2626 srv_debug(0, xs_fmt("Ignored message of type '%s'", type));
2627
2628 /* archive the ignored activity */
2629 xs *ntid = tid(0);
2630 xs *fn = xs_fmt("%s/ignored/%s.json", srv_basedir, ntid);
2631 FILE *f;
2632
2633 if ((f = fopen(fn, "w")) != NULL) {
2634 xs_json_dump(msg, 4, f);
2635 fclose(f);
2636 }
2637
2638 return -1;
2639 }
2640
2641 if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) {
2642 srv_log(xs_fmt("Dropping EmojiReact from %s due to admin configuration", actor));
2643 return -1;
2644 }
2645
2646 const char *object, *utype;
2647
2648 object = xs_dict_get(msg, "object");
2649 if (object != NULL && xs_type(object) == XSTYPE_DICT)
2650 utype = xs_dict_get(object, "type");
2651 else
2652 utype = "(null)";
2653
2654 /* special case for Delete messages */
2655 if (strcmp(type, "Delete") == 0) {
2656 /* if the actor is not here, do not even try */
2657 if (!object_here(actor)) {
2658 srv_debug(1, xs_fmt("dropped 'Delete' message from unknown actor '%s'", actor));
2659 return -1;
2660 }
2661
2662 /* discard crap */
2663 if (xs_is_null(object)) {
2664 srv_log(xs_fmt("dropped 'Delete' message with invalid object from actor '%s'", actor));
2665 return -1;
2666 }
2667
2668 /* also discard if the object to be deleted is not here */
2669 const char *obj_id = object;
2670 if (xs_type(obj_id) == XSTYPE_DICT)
2671 obj_id = xs_dict_get(obj_id, "id");
2672
2673 if (xs_is_null(obj_id) || !object_here(obj_id)) {
2674 srv_debug(1, xs_fmt("dropped 'Delete' message from unknown object '%s'", obj_id));
2675 return -1;
2676 }
2677 }
2678
2679 /* bring the actor */
2680 a_status = actor_request(snac, actor, &actor_o);
2681
2682 /* do not retry permanent failures */
2683 if (a_status == HTTP_STATUS_NOT_FOUND
2684 || a_status == HTTP_STATUS_GONE
2685 || a_status < 0) {
2686 srv_debug(1, xs_fmt("dropping message due to actor error %s %d", actor, a_status));
2687 return -1;
2688 }
2689
2690 if (!valid_status(a_status)) {
2691 /* do not retry 'Delete' messages */
2692 if (strcmp(type, "Delete") == 0) {
2693 srv_debug(1, xs_fmt("dropping 'Delete' message due to actor error %s %d", actor, a_status));
2694 return -1;
2695 }
2696
2697 /* other actor download errors */
2698
2699 /* the actor may require a signed request; propagate if no user is set */
2700 if (snac == NULL)
2701 return 2;
2702
2703 /* may need a retry */
2704 srv_debug(0, xs_fmt("error requesting actor %s %d -- retry later", actor, a_status));
2705 return 0;
2706 }
2707
2708 /* check the signature */
2709 xs *sig_err = NULL;
2710 xs *key_id = NULL;
2711
2712 if (!check_signature(req, &sig_err, &key_id)) {
2713 srv_log(xs_fmt("bad signature %s (%s)", actor, sig_err));
2714
2715 srv_archive_error("check_signature", sig_err, req, msg);
2716 return -1;
2717 }
2718
2719 /* if no user is set, no further checks can be done; propagate */
2720 if (snac == NULL)
2721 return 2;
2722
2723 /* reject messages that are not for this user */
2724 if (!is_msg_for_me(snac, msg)) {
2725 snac_debug(snac, 1, xs_fmt("message from %s of type '%s' not for us", actor, type));
2726
2727 return 1;
2728 }
2729
2730 /* if it's a DM from someone we don't follow, reject the message */
2731 if (xs_type(xs_dict_get(snac->config, "drop_dm_from_unknown")) == XSTYPE_TRUE) {
2732 if (strcmp(utype, "Note") == 0 && !is_msg_public(msg) &&
2733 !following_check(snac, actor)) {
2734 snac_log(snac, xs_fmt("DM rejected from unknown actor %s", actor));
2735
2736 return 1;
2737 }
2738 }
2739
2740 /* check the minimum acceptable account age */
2741 int min_account_age = xs_number_get(xs_dict_get(srv_config, "min_account_age"));
2742
2743 if (min_account_age > 0) {
2744 const char *actor_date = xs_dict_get(actor_o, "published");
2745 if (!xs_is_null(actor_date)) {
2746 time_t actor_t = xs_parse_iso_date(actor_date, 0);
2747
2748 if (actor_t < 950000000) {
2749 snac_log(snac, xs_fmt("rejected activity from %s (suspicious date, %s)",
2750 actor, actor_date));
2751
2752 return 1;
2753 }
2754
2755 if (actor_t > 0) {
2756 int td = (int)(time(NULL) - actor_t);
2757
2758 snac_debug(snac, 2, xs_fmt("actor %s age: %d seconds", actor, td));
2759
2760 if (td < min_account_age) {
2761 snac_log(snac, xs_fmt("rejected activity from %s (too new, %d seconds)",
2762 actor, td));
2763
2764 return 1;
2765 }
2766 }
2767 }
2768 else
2769 snac_debug(snac, 1, xs_fmt("warning: empty or null creation date for %s", actor));
2770 }
2771
2772 if (strcmp(type, "Follow") == 0) { /** **/
2773 const char *id = xs_dict_get(msg, "id");
2774
2775 if (xs_is_null(id)) {
2776 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
2777 }
2778 else
2779 if (!follower_check(snac, actor)) {
2780 /* ensure the actor object is here */
2781 if (!object_here(actor)) {
2782 xs *actor_obj = NULL;
2783 actor_request(snac, actor, &actor_obj);
2784 object_add(actor, actor_obj);
2785 }
2786
2787 if (xs_is_true(xs_dict_get(snac->config, "approve_followers"))) {
2788 pending_add(snac, actor, msg);
2789
2790 snac_log(snac, xs_fmt("new pending follower approval %s", actor));
2791 }
2792 else {
2793 /* automatic following */
2794 xs *f_msg = xs_dup(msg);
2795 xs *reply = msg_accept(snac, f_msg, actor);
2796
2797 post_message(snac, actor, reply);
2798
2799 if (xs_is_null(xs_dict_get(f_msg, "published"))) {
2800 /* add a date if it doesn't include one (Mastodon) */
2801 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
2802 f_msg = xs_dict_set(f_msg, "published", date);
2803 }
2804
2805 timeline_add(snac, id, f_msg);
2806
2807 follower_add(snac, actor);
2808
2809 snac_log(snac, xs_fmt("new follower %s", actor));
2810 }
2811
2812 do_notify = 1;
2813 }
2814 else
2815 snac_log(snac, xs_fmt("repeated 'Follow' from %s", actor));
2816 }
2817 else
2818 if (strcmp(type, "Undo") == 0) { /** **/
2819 const char *id = xs_dict_get(object, "object");
2820 const char *content = xs_dict_get(object, "content");
2821 /* misskey sends emojis as like + tag */
2822 xs *cd = xs_dup(content);
2823 const char *sna = cd;
2824 const xs_dict *tag = xs_dict_get(object, "tag");
2825 unsigned int utf = xs_utf8_dec((const char **)&sna);
2826
2827 int isEmoji = 0;
2828 if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
2829 isEmoji = 1;
2830
2831 if (xs_type(object) != XSTYPE_DICT) {
2832 snac_debug(snac, 1, xs_fmt("undo: overriding utype %s | %s | %s",
2833 utype, id, actor));
2834 utype = "Follow";
2835 }
2836
2837 if (strcmp(actor, key_id)) {
2838 snac_log(snac, xs_fmt("Undo: mismatched actor '%s' and key '%s'", actor, key_id));
2839 }
2840 else
2841 if (strcmp(utype, "Follow") == 0) { /** **/
2842 if (!id) {
2843 snac_log(snac, xs_fmt("no id (msg.object.object) when "
2844 "handling follow: %s", actor));
2845 return 1;
2846 }
2847 if (strcmp(id, snac->actor) != 0 && strcmp(id, snac->actor_alt) != 0)
2848 snac_debug(snac, 1, xs_fmt("Undo + Follow from %s not for us (%s)", actor, id));
2849 else
2850 if (valid_status(follower_del(snac, actor))) {
2851 snac_log(snac, xs_fmt("no longer following us %s", actor));
2852 do_notify = 1;
2853 }
2854 else
2855 if (pending_check(snac, actor)) {
2856 pending_del(snac, actor);
2857 snac_log(snac, xs_fmt("cancelled pending follow from %s", actor));
2858 }
2859 else
2860 snac_log(snac, xs_fmt("error deleting follower %s", actor));
2861 }
2862 /* *key emojis are like w/ Emoji tag */
2863 else
2864 if (isEmoji || strcmp(utype, "EmojiReact") == 0) {
2865 const xs_val *mid = xs_dict_get(object, "id");
2866 int status = object_rm_emoji_react(id, mid);
2867 /* ensure *key notifications type */
2868 utype = "EmojiReact";
2869
2870 snac_log(snac, xs_fmt("Undo 'EmojiReact' for %s %d", id, status));
2871 }
2872 else
2873 if (strcmp(utype, "Like") == 0) { /** **/
2874 int status = object_unadmire(id, actor, 1);
2875
2876 snac_log(snac, xs_fmt("Undo '%s' for %s %d", utype, id, status));
2877 }
2878 else
2879 if (strcmp(utype, "Announce") == 0) { /** **/
2880 int status = HTTP_STATUS_OK;
2881
2882 /* commented out: if a followed user boosts something that
2883 is requested and then unboosts, the post remains here,
2884 but with no apparent reason, and that is confusing */
2885 //status = object_unadmire(id, actor, 0);
2886
2887 snac_log(snac, xs_fmt("Unboost for %s %d", id, status));
2888 }
2889 else
2890 snac_debug(snac, 1, xs_fmt("ignored 'Undo' for object type '%s'", utype));
2891 }
2892 else
2893 if (strcmp(type, "Create") == 0) { /** **/
2894 if (is_muted(snac, actor)) {
2895 snac_log(snac, xs_fmt("ignored 'Create' + '%s' from muted actor %s", utype, actor));
2896 return 1;
2897 }
2898
2899 if (xs_match(utype, "Note|Article")) { /** **/
2900 const char *id = xs_dict_get(object, "id");
2901 const char *in_reply_to = get_in_reply_to(object);
2902 const char *atto = get_atto(object);
2903 xs *wrk = NULL;
2904
2905 if (xs_is_null(id))
2906 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
2907 else
2908 if (xs_is_null(atto))
2909 snac_log(snac, xs_fmt("malformed message: no 'attributedTo' field"));
2910 else
2911 if (!xs_is_null(in_reply_to) && is_hidden(snac, in_reply_to)) {
2912 snac_debug(snac, 0, xs_fmt("dropped reply %s to hidden post %s", id, in_reply_to));
2913 }
2914 else {
2915 if (content_match("filter_reject.txt", object)) {
2916 snac_log(snac, xs_fmt("rejected by content %s", id));
2917 return 1;
2918 }
2919
2920 if (strcmp(key_id, atto) != 0) {
2921 if (following_check(snac, key_id)) {
2922 snac_debug(snac, 1, xs_fmt("Warning: trusting key_id %s for post %s from %s atto", key_id, id, atto));
2923 }
2924 else {
2925 snac_debug(snac, 1, xs_fmt("Warning: %s key_id != atto (%s != %s) -- deferring request", id, key_id, atto));
2926 enqueue_object_request(snac, id, 0);
2927 return 1;
2928 }
2929 }
2930
2931 if (strcmp(actor, atto) != 0)
2932 snac_log(snac, xs_fmt("SUSPICIOUS: actor != atto (%s != %s)", actor, atto));
2933
2934 timeline_request(snac, &in_reply_to, &wrk, 0);
2935
2936 const char *quoted_id = xs_or(xs_dict_get(object, "quoteUri"), xs_dict_get(object, "quoteUrl"));
2937
2938 if (xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/
2939 xs *quoted_post = NULL;
2940 int status;
2941
2942 if (valid_status(status = activitypub_request(snac, quoted_id, "ed_post))) {
2943 /* got quoted post */
2944 object_add(quoted_id, quoted_post);
2945 }
2946
2947 snac_debug(snac, 1, xs_fmt("retrieving quoted post %s %d", quoted_id, status));
2948 }
2949
2950 if (timeline_add(snac, id, object)) {
2951 snac_log(snac, xs_fmt("new '%s' %s %s", utype, actor, id));
2952 do_notify = 1;
2953 }
2954
2955 /* if it has a "name" field, it may be a vote for a question */
2956 const char *name = xs_dict_get(object, "name");
2957
2958 if (!xs_is_null(name) && *name && !xs_is_null(in_reply_to) && *in_reply_to)
2959 update_question(snac, in_reply_to);
2960 }
2961 }
2962 else
2963 if (strcmp(utype, "Question") == 0) { /** **/
2964 const char *id = xs_dict_get(object, "id");
2965
2966 if (xs_is_null(id))
2967 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
2968 else
2969 if (timeline_add(snac, id, object))
2970 snac_log(snac, xs_fmt("new 'Question' %s %s", actor, id));
2971 }
2972 else
2973 if (xs_match(utype, "Audio|Video|Event")) { /** **/
2974 const char *id = xs_dict_get(object, "id");
2975
2976 if (xs_is_null(id))
2977 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
2978 else
2979 if (timeline_add(snac, id, object))
2980 snac_log(snac, xs_fmt("new '%s' %s %s", utype, actor, id));
2981 }
2982 else
2983 snac_debug(snac, 1, xs_fmt("ignored 'Create' for object type '%s'", utype));
2984 }
2985 else
2986 if (strcmp(type, "Accept") == 0) { /** **/
2987 if (strcmp(utype, "(null)") == 0) {
2988 const char *obj_id = xs_dict_get(msg, "object");
2989
2990 /* if the accepted object id is a string that may
2991 be created by us, it's a follow */
2992 if (xs_type(obj_id) == XSTYPE_STRING &&
2993 xs_startswith(obj_id, srv_baseurl) &&
2994 xs_endswith(obj_id, "/Follow"))
2995 utype = "Follow";
2996 }
2997
2998 if (strcmp(utype, "Follow") == 0) { /** **/
2999 if (following_check(snac, actor)) {
3000 following_add(snac, actor, msg);
3001 snac_log(snac, xs_fmt("confirmed follow from %s", actor));
3002
3003 /* request a bit of this fellow's outbox */
3004 enqueue_collect_outbox(snac, actor);
3005 }
3006 else
3007 snac_log(snac, xs_fmt("spurious follow accept from %s", actor));
3008 }
3009 else
3010 if (strcmp(utype, "Create") == 0) {
3011 /* some implementations send Create confirmations, go figure */
3012 snac_debug(snac, 1, xs_dup("ignored 'Accept' + 'Create'"));
3013 }
3014 else {
3015 xs_str *e = xs_fmt("ignored 'Accept' for object type '%s'", utype);
3016 srv_archive_error("accept", e, req, msg);
3017 snac_log(snac, e);
3018 }
3019 }
3020 else
3021 if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0) { /** **/
3022 /* misskey sends emojis as Like + tag.
3023 * It is easier to handle them both at the same time. */
3024 const char *sna = xs_dict_get(msg, "content");
3025 const xs_dict *tag = xs_dict_get(msg, "tag");
3026 unsigned int utf = xs_utf8_dec((const char **)&sna);
3027
3028 if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
3029 type = "EmojiReact";
3030
3031 if (xs_type(object) == XSTYPE_DICT)
3032 object = xs_dict_get(object, "id");
3033
3034 if (xs_is_null(object))
3035 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
3036 else
3037 if (timeline_admire(snac, object, actor, 1, msg) == HTTP_STATUS_CREATED)
3038 snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object));
3039 else
3040 snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object));
3041
3042 do_notify = 1;
3043 }
3044 else
3045 if (strcmp(type, "Announce") == 0) { /** **/
3046 if (xs_type(object) == XSTYPE_DICT)
3047 object = xs_dict_get(object, "id");
3048
3049 if (xs_is_null(object))
3050 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
3051 else
3052 if (is_muted(snac, actor) && !is_msg_mine(snac, object))
3053 snac_log(snac, xs_fmt("dropped 'Announce' from muted actor %s", actor));
3054 else
3055 if (is_limited(snac, actor) && !is_msg_mine(snac, object))
3056 snac_log(snac, xs_fmt("dropped 'Announce' from limited actor %s", actor));
3057 else {
3058 xs *a_msg = NULL;
3059 xs *wrk = NULL;
3060
3061 timeline_request(snac, &object, &wrk, 0);
3062
3063 if (valid_status(object_get(object, &a_msg))) {
3064 const char *who = get_atto(a_msg);
3065
3066 /* got the admired object: instance is [back] online */
3067 instance_failure(object, OP_DEL);
3068
3069 if (who && !is_muted(snac, who)) {
3070 /* bring the actor */
3071 xs *who_o = NULL;
3072
3073 if (valid_status(actor_request(snac, who, &who_o))) {
3074 /* don't account as such announces by our own relay */
3075 xs *this_relay = xs_fmt("%s/relay", srv_baseurl);
3076
3077 if (strcmp(actor, this_relay) != 0) {
3078 if (valid_status(timeline_admire(snac, object, actor, 0, a_msg)))
3079 snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
3080 else
3081 snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
3082 actor, object));
3083 }
3084
3085 /* distribute the post with the actor as 'proxy' */
3086 list_distribute(snac, actor, a_msg);
3087
3088 /* distribute the post to users following these hashtags */
3089 followed_hashtag_distribute(a_msg);
3090
3091 /* actor is [back] alive */
3092 actor_failure(who, OP_DEL);
3093
3094 do_notify = 1;
3095 }
3096 else
3097 snac_debug(snac, 1, xs_fmt("dropped 'Announce' on actor request error %s", who));
3098 }
3099 else
3100 snac_log(snac, xs_fmt("ignored 'Announce' about muted actor %s", who));
3101 }
3102 else
3103 snac_debug(snac, 2, xs_fmt("error requesting 'Announce' object %s", object));
3104 }
3105 }
3106 else
3107 if (strcmp(type, "Update") == 0) { /** **/
3108 if (xs_match(utype, "Person|Service|Application")) { /** **/
3109 if (strcmp(actor, key_id) == 0) {
3110 actor_add(actor, xs_dict_get(msg, "object"));
3111 timeline_touch(snac);
3112
3113 snac_log(snac, xs_fmt("updated actor %s", actor));
3114 }
3115 else {
3116 /* actor / key mismatch: don't accept blindly, but request an actor update
3117 from the original source, as the Update may come from a relay and be legit */
3118 enqueue_actor_refresh(snac, actor, -10);
3119
3120 snac_log(snac, xs_fmt("Update: mismatched actor '%s' and key '%s'", actor, key_id));
3121 }
3122 }
3123 else
3124 if (xs_match(utype, "Note|Page|Article|Video|Audio|Event")) { /** **/
3125 const char *id = xs_dict_get(object, "id");
3126
3127 if (xs_is_null(id))
3128 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
3129 else
3130 if (object_here(id)) {
3131 const char *atto = get_atto(object);
3132
3133 if (atto == NULL)
3134 snac_log(snac, xs_fmt("ignored post 'Update' with no attributedTo %s", id));
3135 else
3136 if (strcmp(atto, key_id) != 0) {
3137 /* actor / key mismatch: request the object from the original source */
3138 enqueue_object_request(snac, id, -10);
3139
3140 snac_log(snac, xs_fmt("Update: mismatched attributedTo '%s' and key '%s'", atto, key_id));
3141 }
3142 else
3143 if (xs_startswith(id, srv_baseurl) && !xs_startswith(id, actor))
3144 snac_log(snac, xs_fmt("ignored incorrect 'Update' %s %s", actor, id));
3145 else {
3146 object_add_ow(id, object);
3147 timeline_touch(snac);
3148
3149 snac_log(snac, xs_fmt("updated '%s' %s", utype, id));
3150 }
3151 }
3152 else
3153 snac_log(snac, xs_fmt("dropped update for unknown '%s' %s", utype, id));
3154 }
3155 else
3156 if (strcmp(utype, "Question") == 0) { /** **/
3157 const char *id = xs_dict_get(object, "id");
3158 const char *closed = xs_dict_get(object, "closed");
3159
3160 if (xs_is_null(id))
3161 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
3162 else {
3163 object_add_ow(id, object);
3164 timeline_touch(snac);
3165
3166 snac_log(snac, xs_fmt("%s poll %s", closed == NULL ? "updated" : "closed", id));
3167
3168 if (closed != NULL)
3169 do_notify = 1;
3170 }
3171 }
3172 else {
3173 srv_archive_error("unsupported_update", "unsupported_update", req, msg);
3174
3175 snac_log(snac, xs_fmt("ignored 'Update' for object type '%s'", utype));
3176 }
3177 }
3178 else
3179 if (strcmp(type, "Delete") == 0) { /** **/
3180 if (xs_type(object) == XSTYPE_DICT)
3181 object = xs_dict_get(object, "id");
3182
3183 xs *obj_data = NULL;
3184
3185 if (xs_is_null(object))
3186 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
3187 else
3188 if (valid_status(object_get(object, &obj_data))) {
3189 const char *atto = get_atto(obj_data);
3190
3191 if (atto == NULL)
3192 snac_log(snac, xs_fmt("ignored 'Delete' for object without attributedTo %s", object));
3193 else
3194 if (strcmp(atto, key_id) != 0) {
3195 /* don't call srv_archive_error() on this, because it can be a delete from a relay */
3196 snac_log(snac, xs_fmt("Delete: mismatched attributedTo '%s' and key '%s'", atto, key_id));
3197 }
3198 else
3199 if (xs_startswith(object, srv_baseurl) && !is_msg_mine(snac, object))
3200 snac_log(snac, xs_fmt("ignored incorrect 'Delete' %s %s", actor, object));
3201 else {
3202 timeline_del(snac, object);
3203 snac_debug(snac, 1, xs_fmt("new 'Delete' %s %s", actor, object));
3204 }
3205 }
3206 else
3207 snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
3208 }
3209 else
3210 if (strcmp(type, "Pong") == 0) { /** **/
3211 snac_log(snac, xs_fmt("'Pong' received from %s", actor));
3212 }
3213 else
3214 if (strcmp(type, "Ping") == 0) { /** **/
3215 const char *id = xs_dict_get(msg, "id");
3216
3217 snac_log(snac, xs_fmt("'Ping' requested from %s", actor));
3218
3219 if (!xs_is_null(id)) {
3220 xs *rsp = msg_pong(snac, actor, id);
3221
3222 enqueue_output_by_actor(snac, rsp, actor, 0);
3223 }
3224 }
3225 else
3226 if (strcmp(type, "Block") == 0) { /** **/
3227 snac_debug(snac, 1, xs_fmt("'Block' received from %s", actor));
3228
3229 /* should we MUTE the actor back? */
3230 /* mute(snac, actor); */
3231
3232 if (!xs_is_true(xs_dict_get(srv_config, "disable_block_notifications")))
3233 do_notify = 1;
3234 }
3235 else
3236 if (strcmp(type, "Move") == 0) { /** **/
3237 do_notify = 1;
3238
3239 const char *old_account = xs_dict_get(msg, "object");
3240 const char *new_account = xs_dict_get(msg, "target");
3241
3242 if (xs_is_string(old_account) && xs_is_string(new_account)) {
3243 if (strcmp(old_account, key_id) != 0) {
3244 xs_str *e = xs_fmt("'Move': mismatched old_account %s and key %s", old_account, key_id);
3245 srv_archive_error("move_old_account_key_mismatch", e, req, msg);
3246 snac_log(snac, e);
3247 }
3248 else
3249 if (following_check(snac, old_account)) {
3250 xs *n_actor = NULL;
3251
3252 if (valid_status(object_get(new_account, &n_actor))) {
3253 const xs_list *aka = xs_dict_get(n_actor, "alsoKnownAs");
3254
3255 if (xs_type(aka) == XSTYPE_LIST) {
3256 if (xs_list_in(aka, old_account) != -1) {
3257 /* all conditions met! */
3258
3259 /* follow new account */
3260 xs *f_msg = msg_follow(snac, new_account);
3261
3262 if (f_msg != NULL) {
3263 const char *new_actor = xs_dict_get(f_msg, "object");
3264 following_add(snac, new_actor, f_msg);
3265 enqueue_output_by_actor(snac, f_msg, new_actor, 0);
3266
3267 snac_log(snac, xs_fmt("'Move': following %s", new_account));
3268 }
3269
3270 /* unfollow old account */
3271 xs *of_msg = NULL;
3272
3273 if (valid_status(following_get(snac, old_account, &of_msg))) {
3274 xs *uf_msg = msg_undo(snac, xs_dict_get(of_msg, "object"));
3275 following_del(snac, old_account);
3276 enqueue_output_by_actor(snac, uf_msg, old_account, 0);
3277
3278 snac_log(snac, xs_fmt("'Move': unfollowing %s", old_account));
3279 }
3280 }
3281 else
3282 snac_log(snac, xs_fmt("'Move' error: old actor %s not found in %s 'alsoKnownAs'",
3283 old_account, new_account));
3284 }
3285 else
3286 snac_log(snac, xs_fmt("'Move' error: cannot get %s 'alsoKnownAs'", new_account));
3287 }
3288 else {
3289 snac_log(snac, xs_fmt("'Move' error: cannot get new actor %s", new_account));
3290
3291 /* may be a server hiccup, retry later */
3292 return 0;
3293 }
3294 }
3295 else
3296 snac_log(snac, xs_fmt("'Move' error: actor %s is not being followed", old_account));
3297 }
3298 else {
3299 snac_log(snac, xs_fmt("'Move' error: malformed message from %s", actor));
3300 srv_archive_error("move", "move", req, msg);
3301 }
3302 }
3303 else {
3304 srv_archive_error("unsupported_type", "unsupported_type", req, msg);
3305
3306 snac_debug(snac, 1, xs_fmt("process_input_message type '%s' ignored", type));
3307 }
3308
3309 if (do_notify) {
3310 notify(snac, type, utype, actor, msg);
3311
3312 timeline_touch(snac);
3313 }
3314
3315 return 1;
3316}
3317
3318
3319int send_email(const xs_dict *mailinfo)
3320/* invoke curl */
3321{
3322 const char *url = xs_dict_get(srv_config, "smtp_url");
3323
3324 if (!xs_is_string(url) || *url == '\0') {
3325 /* revert back to old sendmail pipe behaviour */
3326 const char *msg = xs_dict_get(mailinfo, "body");
3327 FILE *f;
3328 int status;
3329 int fds[2];
3330 pid_t pid;
3331
3332 if (pipe(fds) == -1) return -1;
3333 pid = vfork();
3334 if (pid == -1) return -1;
3335 else if (pid == 0) {
3336 dup2(fds[0], 0);
3337 close(fds[0]);
3338 close(fds[1]);
3339 execl("/usr/sbin/sendmail", "sendmail", "-t", (char *) NULL);
3340 _exit(1);
3341 }
3342 close(fds[0]);
3343 if ((f = fdopen(fds[1], "w")) == NULL) {
3344 close(fds[1]);
3345 return -1;
3346 }
3347 fprintf(f, "%s\n", msg);
3348 fclose(f);
3349 if (waitpid(pid, &status, 0) == -1) return -1;
3350 return status;
3351 }
3352
3353 const char
3354 *user = xs_dict_get(srv_config, "smtp_username"),
3355 *pass = xs_dict_get(srv_config, "smtp_password"),
3356 *from = xs_dict_get(mailinfo, "from"),
3357 *to = xs_dict_get(mailinfo, "to"),
3358 *body = xs_dict_get(mailinfo, "body");
3359
3360 if (url == NULL || *url == '\0')
3361 url = "smtp://localhost";
3362
3363 int smtp_port = parse_port(url, NULL);
3364
3365 return xs_smtp_request(url, user, pass, from, to, body, smtp_port == 465 || smtp_port == 587);
3366}
3367
3368
3369void process_user_queue_item(snac *user, xs_dict *q_item)
3370/* processes an item from the user queue */
3371{
3372 const char *type;
3373 int queue_retry_max = xs_number_get(xs_dict_get(srv_config, "queue_retry_max"));
3374
3375 if ((type = xs_dict_get(q_item, "type")) == NULL)
3376 type = "output";
3377
3378 if (strcmp(type, "message") == 0) {
3379 const xs_dict *msg = xs_dict_get(q_item, "message");
3380 xs *rcpts = recipient_list(user, msg, 1);
3381 xs_set inboxes;
3382 const xs_str *actor;
3383
3384 xs_set_init(&inboxes);
3385
3386 /* add this shared inbox first */
3387 xs *this_shared_inbox = xs_fmt("%s/shared-inbox", srv_baseurl);
3388 xs_set_add(&inboxes, this_shared_inbox);
3389 enqueue_output(user, msg, this_shared_inbox, 0, 0);
3390
3391 /* iterate the recipients */
3392 xs_list_foreach(rcpts, actor) {
3393 /* local users were served by this_shared_inbox */
3394 if (!xs_startswith(actor, srv_baseurl)) {
3395 xs *inbox = get_actor_inbox(actor, 1);
3396
3397 if (inbox != NULL) {
3398 /* add to the set and, if it's not there, send message */
3399 if (xs_set_add(&inboxes, inbox) == 1)
3400 enqueue_output(user, msg, inbox, 0, 0);
3401 }
3402 else
3403 snac_log(user, xs_fmt("cannot find inbox for %s", actor));
3404 }
3405 }
3406
3407 /* if it's a public note or question, send to the collected inboxes */
3408 if (xs_match(xs_dict_get_def(msg, "type", ""), "Create|Update") && is_msg_public(msg)) {
3409 if (xs_type(xs_dict_get(srv_config, "disable_inbox_collection")) != XSTYPE_TRUE) {
3410 xs *shibx = inbox_list();
3411 const xs_str *inbox;
3412
3413 xs_list_foreach(shibx, inbox) {
3414 if (xs_set_add(&inboxes, inbox) == 1)
3415 enqueue_output(user, msg, inbox, 0, 0);
3416 }
3417 }
3418 }
3419
3420 xs_set_free(&inboxes);
3421
3422 /* relay this note */
3423 if (is_msg_public(msg) && strcmp(user->uid, "relay") != 0) { /* avoid loops */
3424 snac relay;
3425 if (user_open(&relay, "relay")) {
3426 /* a 'relay' user exists */
3427 const char *type = xs_dict_get(msg, "type");
3428
3429 if (xs_is_string(type) && strcmp(type, "Create") == 0) {
3430 const xs_val *object = xs_dict_get(msg, "object");
3431
3432 if (xs_is_dict(object)) {
3433 object = xs_dict_get(object, "id");
3434
3435 snac_debug(&relay, 1, xs_fmt("relaying message %s", object));
3436
3437 xs *boost = msg_admiration(&relay, object, "Announce");
3438 enqueue_message(&relay, boost);
3439 }
3440 }
3441
3442 user_free(&relay);
3443 }
3444 }
3445 }
3446 else
3447 if (strcmp(type, "input") == 0) {
3448 /* process the message */
3449 const xs_dict *msg = xs_dict_get(q_item, "message");
3450 const xs_dict *req = xs_dict_get(q_item, "req");
3451 int retries = xs_number_get(xs_dict_get(q_item, "retries"));
3452
3453 if (xs_is_null(msg))
3454 return;
3455
3456 if (!process_input_message(user, msg, req)) {
3457 if (retries > queue_retry_max)
3458 snac_log(user, xs_fmt("input giving up"));
3459 else {
3460 /* reenqueue */
3461 enqueue_input(user, msg, req, retries + 1);
3462 snac_log(user, xs_fmt("input requeue #%d", retries + 1));
3463 }
3464 }
3465 }
3466 else
3467 if (strcmp(type, "close_question") == 0) {
3468 /* the time for this question has ended */
3469 const char *id = xs_dict_get(q_item, "message");
3470
3471 if (!xs_is_null(id))
3472 update_question(user, id);
3473 }
3474 else
3475 if (strcmp(type, "object_request") == 0) {
3476 const char *id = xs_dict_get(q_item, "message");
3477
3478 if (!xs_is_null(id)) {
3479 int status;
3480 xs *data = NULL;
3481
3482 status = activitypub_request(user, id, &data);
3483
3484 if (valid_status(status))
3485 object_add_ow(id, data);
3486
3487 snac_debug(user, 1, xs_fmt("object_request %s %d", id, status));
3488 }
3489 }
3490 else
3491 if (strcmp(type, "verify_links") == 0) {
3492 verify_links(user);
3493 }
3494 else
3495 if (strcmp(type, "actor_refresh") == 0) {
3496 const char *actor = xs_dict_get(q_item, "actor");
3497 double mtime = object_mtime(actor);
3498
3499 if (actor_failure(actor, OP_CHECK) == -1) {
3500 /* actor is broken beyond repair */
3501 snac_debug(user, 1, xs_fmt("actor_refresh skipped broken actor %s", actor));
3502 }
3503 else
3504 /* only refresh if it was refreshed more than an hour ago */
3505 if (mtime + 3600.0 < (double) time(NULL)) {
3506 xs *actor_o = NULL;
3507 int status;
3508
3509 if (valid_status((status = activitypub_request(user, actor, &actor_o)))) {
3510 /* refresh collection counts with throttling to prevent resource exhaustion */
3511 actor_o = actor_get_collections(user, actor_o, 1);
3512
3513 actor_add(actor, actor_o);
3514
3515 /* mark actor and instance as working */
3516 actor_failure(actor, OP_DEL);
3517 instance_failure(actor, OP_DEL);
3518 }
3519 else {
3520 if (status == HTTP_STATUS_GONE || status == HTTP_STATUS_NOT_FOUND) {
3521 actor_failure(actor, OP_ADD);
3522 snac_log(user, xs_fmt("actor_refresh marking actor %s as broken %d", actor, status));
3523 }
3524 else {
3525 actor_failure(actor, OP_DEL);
3526 object_touch(actor);
3527 }
3528 }
3529
3530 snac_log(user, xs_fmt("actor_refresh %s %d", actor, status));
3531 }
3532 }
3533 else
3534 if (strcmp(type, "notify_webhook") == 0) {
3535 const char *webhook = xs_dict_get(user->config, "notify_webhook");
3536
3537 if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/
3538 const xs_dict *msg = xs_dict_get(q_item, "message");
3539 int retries = xs_number_get(xs_dict_get(q_item, "retries"));
3540
3541 xs *hdrs = xs_dict_new();
3542
3543 hdrs = xs_dict_set(hdrs, "content-type", "application/json");
3544 hdrs = xs_dict_set(hdrs, "user-agent", USER_AGENT);
3545
3546 xs *body = xs_json_dumps(msg, 4);
3547
3548 int status;
3549 xs *rsp = xs_http_request("POST", webhook, hdrs, body, strlen(body), &status, NULL, NULL, 0);
3550
3551 snac_debug(user, 0, xs_fmt("webhook post %s %d", webhook, status));
3552
3553 if (!valid_status(status)) {
3554 retries++;
3555
3556 if (retries > queue_retry_max)
3557 snac_debug(user, 0, xs_fmt("webhook post giving up %s", webhook));
3558 else {
3559 snac_debug(user, 0, xs_fmt("webhook post requeue %s %d", webhook, retries));
3560
3561 enqueue_notify_webhook(user, msg, retries);
3562 }
3563 }
3564 }
3565 }
3566 else
3567 if (strcmp(type, "collect_replies") == 0) {
3568 const char *post = xs_dict_get(q_item, "message");
3569
3570 collect_replies(user, post);
3571 }
3572 else
3573 if (strcmp(type, "collect_outbox") == 0) {
3574 const char *actor_id = xs_dict_get(q_item, "message");
3575
3576 collect_outbox(user, actor_id);
3577 }
3578 else
3579 snac_log(user, xs_fmt("unexpected user q_item type '%s'", type));
3580}
3581
3582
3583int process_user_queue(snac *snac)
3584/* processes a user's queue */
3585{
3586 int cnt = 0;
3587 xs *list = user_queue(snac);
3588
3589 xs_list *p = list;
3590 const xs_str *fn;
3591
3592 while (xs_list_iter(&p, &fn)) {
3593 xs *q_item = dequeue(fn);
3594
3595 if (q_item == NULL)
3596 continue;
3597
3598 process_user_queue_item(snac, q_item);
3599 cnt++;
3600 }
3601
3602 scheduled_process(snac);
3603
3604 return cnt;
3605}
3606
3607
3608xs_str *str_status(int status)
3609{
3610 return xs_fmt("%d %s", status, status < 0 ? xs_curl_strerr(status) : xs_http_status_text(status));
3611}
3612
3613
3614void process_queue_item(xs_dict *q_item)
3615/* processes an item from the global queue */
3616{
3617 const char *type = xs_dict_get(q_item, "type");
3618 int queue_retry_max = xs_number_get(xs_dict_get(srv_config, "queue_retry_max"));
3619
3620 if (strcmp(type, "output") == 0) {
3621 int status;
3622 const xs_str *inbox = xs_dict_get(q_item, "inbox");
3623 const xs_str *keyid = xs_dict_get(q_item, "keyid");
3624 const xs_str *seckey = xs_dict_get(q_item, "seckey");
3625 const xs_dict *msg = xs_dict_get(q_item, "message");
3626 int retries = xs_number_get(xs_dict_get(q_item, "retries"));
3627 int p_status = xs_number_get(xs_dict_get(q_item, "p_status"));
3628 xs *payload = NULL;
3629 int p_size = 0;
3630 int timeout = 0;
3631
3632 if (xs_is_null(inbox) || xs_is_null(msg) || xs_is_null(keyid) || xs_is_null(seckey)) {
3633 srv_log(xs_fmt("output message error: missing fields"));
3634 return;
3635 }
3636
3637 if (is_instance_blocked(inbox)) {
3638 srv_debug(0, xs_fmt("discarded output message to blocked instance %s", inbox));
3639 return;
3640 }
3641
3642 if (instance_failure(inbox, OP_CHECK)) {
3643 srv_debug(1, xs_fmt("output message error: too many failures for instance %s", inbox));
3644 return;
3645 }
3646
3647 /* deliver (if previous error status was a timeout, try now longer) */
3648 if (p_status == 599)
3649 timeout = xs_number_get(xs_dict_get_def(srv_config, "queue_timeout_2", "8"));
3650 else
3651 timeout = xs_number_get(xs_dict_get_def(srv_config, "queue_timeout", "6"));
3652
3653 if (timeout == 0)
3654 timeout = 6;
3655
3656 status = send_to_inbox_raw(keyid, seckey, inbox, msg, &payload, &p_size, timeout);
3657
3658 /* register or clear a value for this instance */
3659 instance_failure(inbox, valid_status(status) ? OP_DEL : OP_ADD);
3660
3661 if (payload) {
3662 if (p_size > 1024) {
3663 /* trim the message */
3664 payload = xs_utf8_crop_i(payload, 0, 1024);
3665 payload = xs_str_cat(payload, "...");
3666 }
3667
3668 /* strip ugly control characters */
3669 payload = xs_replace_i(payload, "\n", "");
3670 payload = xs_replace_i(payload, "\r", "");
3671
3672 if (*payload)
3673 payload = xs_str_wrap_i(" [", payload, "]");
3674 }
3675 else
3676 payload = xs_str_new(NULL);
3677
3678 xs *s_status = str_status(status);
3679
3680 srv_log(xs_fmt("output message: sent to inbox %s (%s)%s", inbox, s_status, payload));
3681
3682 if (!valid_status(status)) {
3683 retries++;
3684
3685 /* if it's not the first time it fails with a timeout,
3686 penalize the server by skipping one retry */
3687 if (p_status == status && status == HTTP_STATUS_CLIENT_CLOSED_REQUEST)
3688 retries++;
3689
3690 /* error sending; requeue? */
3691 if (status == HTTP_STATUS_BAD_REQUEST
3692 || status == HTTP_STATUS_NOT_FOUND
3693 || status == HTTP_STATUS_METHOD_NOT_ALLOWED
3694 || status == HTTP_STATUS_GONE
3695 || status == HTTP_STATUS_UNPROCESSABLE_CONTENT
3696 || status < 0)
3697 /* explicit error: discard */
3698 srv_log(xs_fmt("output message: error %s (%s)", inbox, s_status));
3699 else
3700 if (retries > queue_retry_max)
3701 srv_log(xs_fmt("output message: giving up %s (%s)", inbox, s_status));
3702 else {
3703 /* requeue */
3704 enqueue_output_raw(keyid, seckey, msg, inbox, retries, status);
3705 srv_log(xs_fmt("output message: requeue %s #%d", inbox, retries));
3706 }
3707 }
3708 }
3709 else
3710 if (strcmp(type, "email") == 0) {
3711 /* send this email */
3712 const xs_dict *msg = xs_dict_get(q_item, "message");
3713 int retries = xs_number_get(xs_dict_get(q_item, "retries"));
3714
3715 if (!send_email(msg))
3716 srv_debug(1, xs_fmt("email message sent"));
3717 else {
3718 retries++;
3719
3720 if (retries > queue_retry_max)
3721 srv_log(xs_fmt("email giving up (errno: %d)", errno));
3722 else {
3723 /* requeue */
3724 srv_log(xs_fmt(
3725 "email requeue #%d (errno: %d)", retries, errno));
3726
3727 enqueue_email(msg, retries);
3728 }
3729 }
3730 }
3731 else
3732 if (strcmp(type, "telegram") == 0) {
3733 /* send this via telegram */
3734 const char *bot = xs_dict_get(q_item, "bot");
3735 const char *msg = xs_dict_get(q_item, "message");
3736 xs *chat_id = xs_dup(xs_dict_get(q_item, "chat_id"));
3737 int status = 0;
3738
3739 /* chat_id must start with a - */
3740 if (!xs_startswith(chat_id, "-"))
3741 chat_id = xs_str_wrap_i("-", chat_id, NULL);
3742
3743 xs *url = xs_fmt("https:/" "/api.telegram.org/bot%s/sendMessage", bot);
3744 xs *body = xs_fmt("{\"chat_id\":%s,\"text\":\"%s\"}", chat_id, msg);
3745
3746 xs *headers = xs_dict_new();
3747 headers = xs_dict_append(headers, "content-type", "application/json");
3748
3749 xs *rsp = xs_http_request("POST", url, headers,
3750 body, strlen(body), &status, NULL, NULL, 0);
3751 rsp = xs_free(rsp);
3752
3753 srv_debug(0, xs_fmt("telegram post %d", status));
3754 }
3755 else
3756 if (strcmp(type, "ntfy") == 0) {
3757 /* send this via ntfy */
3758 const char *ntfy_server = xs_dict_get(q_item, "ntfy_server");
3759 const char *msg = xs_dict_get(q_item, "message");
3760 const char *ntfy_token = xs_dict_get(q_item, "ntfy_token");
3761 int status = 0;
3762
3763 xs *url = xs_fmt("%s", ntfy_server);
3764 xs *body = xs_fmt("%s", msg);
3765
3766 xs *headers = xs_dict_new();
3767 headers = xs_dict_append(headers, "content-type", "text/plain");
3768 // Append the Authorization header only if ntfy_token is not NULL
3769 if (ntfy_token != NULL) {
3770 headers = xs_dict_append(headers, "Authorization", xs_fmt("Bearer %s", ntfy_token));
3771 }
3772
3773 xs *rsp = xs_http_request("POST", url, headers,
3774 body, strlen(body), &status, NULL, NULL, 0);
3775 rsp = xs_free(rsp);
3776
3777 srv_debug(0, xs_fmt("ntfy post %d", status));
3778 }
3779 else
3780 if (strcmp(type, "purge") == 0) {
3781 srv_log(xs_dup("purge start"));
3782
3783 purge_all();
3784
3785 srv_log(xs_dup("purge end"));
3786 }
3787 else
3788 if (strcmp(type, "input") == 0) {
3789 const xs_dict *msg = xs_dict_get(q_item, "message");
3790 const xs_dict *req = xs_dict_get(q_item, "req");
3791 int retries = xs_number_get(xs_dict_get(q_item, "retries"));
3792
3793 /* do some instance-level checks */
3794 int r = process_input_message(NULL, msg, req);
3795
3796 if (r == 0) {
3797 /* transient error? retry */
3798 if (retries > queue_retry_max)
3799 srv_log(xs_fmt("shared input giving up"));
3800 else {
3801 /* reenqueue */
3802 enqueue_shared_input(msg, req, retries + 1);
3803 srv_log(xs_fmt("shared input requeue #%d", retries + 1));
3804 }
3805 }
3806 else
3807 if (r == 2) {
3808 /* redistribute the input message to all users */
3809 const char *ntid = xs_dict_get(q_item, "ntid");
3810 xs *tmpfn = xs_fmt("%s/tmp/%s.json", srv_basedir, ntid);
3811 FILE *f;
3812
3813 if ((f = fopen(tmpfn, "w")) != NULL) {
3814 xs_json_dump(q_item, 4, f);
3815 fclose(f);
3816 }
3817
3818 xs *users = user_list();
3819 xs_list *p = users;
3820 const char *v;
3821 int cnt = 0;
3822
3823 while (xs_list_iter(&p, &v)) {
3824 snac user;
3825
3826 if (user_open(&user, v)) {
3827 int rsn = is_msg_for_me(&user, msg);
3828 if (rsn) {
3829 xs *fn = xs_fmt("%s/queue/%s.json", user.basedir, ntid);
3830
3831 snac_debug(&user, 1,
3832 xs_fmt("enqueue_input (from shared inbox) %s [%d]", xs_dict_get(msg, "id"), rsn));
3833
3834 if (link(tmpfn, fn) < 0)
3835 srv_log(xs_fmt("link(%s, %s) error", tmpfn, fn));
3836
3837 cnt++;
3838 }
3839
3840 user_free(&user);
3841 }
3842 }
3843
3844 unlink(tmpfn);
3845
3846 if (cnt == 0) {
3847 srv_debug(1, xs_fmt("no valid recipients for %s", xs_dict_get(msg, "id")));
3848 }
3849 }
3850 }
3851 else
3852 if (strcmp(type, "webmention") == 0) {
3853 const xs_dict *msg = xs_dict_get(q_item, "message");
3854 const char *source = xs_dict_get(msg, "id");
3855 const char *content = xs_dict_get(msg, "content");
3856
3857 if (xs_is_string(source) && xs_is_string(content)) {
3858 xs *links = xs_regex_select(content, "\"https?:/" "/[^\"]+");
3859 const char *link;
3860
3861 xs_list_foreach(links, link) {
3862 xs *target = xs_strip_chars_i(xs_dup(link), "\"");
3863
3864 int r = xs_webmention_send(source, target, USER_AGENT);
3865
3866 srv_debug(1, xs_fmt("webmention source=%s target=%s %d", source, target, r));
3867 }
3868 }
3869 }
3870 else
3871 if (strcmp(type, "rss_hashtag_poll") == 0) {
3872 rss_poll_hashtags();
3873 }
3874 else
3875 if (strcmp(type, "fsck") == 0) {
3876 srv_log(xs_fmt("started deferred data integrity check"));
3877 data_fsck();
3878 srv_log(xs_fmt("finished deferred data integrity check"));
3879 }
3880 else
3881 srv_log(xs_fmt("unexpected q_item type '%s'", type));
3882}
3883
3884
3885int process_queue(void)
3886/* processes the global queue */
3887{
3888 int cnt = 0;
3889 xs *list = queue();
3890
3891 xs_list *p = list;
3892 const xs_str *fn;
3893
3894 while (xs_list_iter(&p, &fn)) {
3895 xs *q_item = dequeue(fn);
3896
3897 if (q_item != NULL) {
3898 job_post(q_item, 0);
3899 cnt++;
3900 }
3901 }
3902
3903 return cnt;
3904}
3905
3906
3907/** account migration **/
3908
3909int migrate_account(snac *user)
3910/* migrates this account to a new one (stored in the 'alias' user field) */
3911{
3912 const char *new_account = xs_dict_get(user->config, "alias");
3913
3914 if (xs_type(new_account) != XSTYPE_STRING) {
3915 snac_log(user, xs_fmt("Cannot migrate: alias (destination account) not set"));
3916 return 1;
3917 }
3918
3919 xs *new_actor = NULL;
3920 int status;
3921
3922 if (!valid_status(status = activitypub_request(user, new_account, &new_actor))) {
3923 snac_log(user, xs_fmt("Cannot migrate: error requesting actor %s %d", new_account, status));
3924 return 1;
3925 }
3926
3927 actor_add(new_account, new_actor);
3928
3929 const char *loaka = xs_dict_get(new_actor, "alsoKnownAs");
3930
3931 if (xs_type(loaka) != XSTYPE_LIST) {
3932 snac_log(user, xs_fmt("Cannot migrate: destination account doesn't have any aliases"));
3933 return 1;
3934 }
3935
3936 if (xs_list_in(loaka, user->actor) == -1) {
3937 snac_log(user, xs_fmt("Cannot migrate: destination account doesn't have this one as an alias"));
3938 return 1;
3939 }
3940
3941 xs *move = msg_move(user, new_account);
3942 xs *fwers = follower_list(user);
3943
3944 const char *actor;
3945 xs_list_foreach(fwers, actor) {
3946 /* get the actor inbox, excluding the shared one */
3947 xs *inbox = get_actor_inbox(actor, 0);
3948
3949 if (xs_is_null(inbox))
3950 snac_log(user, xs_fmt("migrate_account: cannot get inbox for actor %s", actor));
3951 else
3952 enqueue_output(user, move, inbox, 0, 0);
3953 }
3954
3955 return 0;
3956}
3957
3958
3959/** HTTP handlers */
3960
3961int activitypub_get_handler(const xs_dict *req, const char *q_path,
3962 char **body, int *b_size, char **ctype)
3963{
3964 int status = HTTP_STATUS_OK;
3965 const char *accept = xs_dict_get(req, "accept");
3966 snac snac;
3967 xs *msg = NULL;
3968
3969 if (accept == NULL)
3970 return 0;
3971
3972 if (xs_str_in(accept, "application/activity+json") == -1 &&
3973 xs_str_in(accept, "application/ld+json") == -1)
3974 return 0;
3975
3976 xs *l = xs_split_n(q_path, "/", 2);
3977
3978 if (xs_list_len(l) <= 1) {
3979 srv_debug(1, xs_fmt("activitypub_get_handler bad path %s", q_path));
3980 return HTTP_STATUS_BAD_REQUEST;
3981 }
3982
3983 const char *uid;
3984 const char *p_path;
3985
3986 uid = xs_list_get(l, 1);
3987 if (!user_open(&snac, uid)) {
3988 /* invalid user */
3989 status = grave(uid, OP_CHECK) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND;
3990 srv_debug(1, xs_fmt("activitypub_get_handler bad user %s %d", uid, status));
3991 return status;
3992 }
3993
3994 p_path = xs_list_get(l, 2);
3995
3996 const xs_dict *q_vars = xs_dict_get(req, "q_vars");
3997
3998 *ctype = "application/activity+json";
3999
4000 int show_contact_metrics = xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"));
4001
4002 if (p_path == NULL) {
4003 /* if there was no component after the user, it's an actor request */
4004 msg = msg_actor(&snac);
4005 *ctype = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
4006
4007 const char *ua = xs_dict_get(req, "user-agent");
4008
4009 snac_debug(&snac, 0, xs_fmt("serving actor [%s]", ua ? ua : "No UA"));
4010 }
4011 else
4012 if (strcmp(p_path, "outbox") == 0 || strcmp(p_path, "featured") == 0) {
4013 xs *id = xs_fmt("%s/%s", snac.actor, p_path);
4014 xs *list = xs_list_new();
4015 const char *v;
4016 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
4017
4018 /* get the public outbox or the pinned list */
4019 xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt, NULL) : pinned_list(&snac);
4020
4021 xs_list_foreach(elems, v) {
4022 xs *i = NULL;
4023
4024 if (valid_status(object_get_by_md5(v, &i))) {
4025 const char *type = xs_dict_get(i, "type");
4026 const char *id = xs_dict_get(i, "id");
4027
4028 if (type && id && strcmp(type, "Note") == 0 && is_msg_mine(&snac, id)) {
4029 if (is_msg_public(i)) {
4030 xs *c_msg = msg_create(&snac, i);
4031 list = xs_list_append(list, c_msg);
4032 }
4033 }
4034 }
4035 }
4036
4037 /* replace the 'orderedItems' with the latest posts */
4038 msg = msg_collection(&snac, id, xs_list_len(list));
4039 msg = xs_dict_set(msg, "orderedItems", list);
4040 }
4041 else
4042 if (strcmp(p_path, "followers") == 0) {
4043 int total = 0;
4044
4045 if (show_contact_metrics) {
4046 total = follower_list_len(&snac);
4047 }
4048
4049 xs *id = xs_fmt("%s/%s", snac.actor, p_path);
4050 msg = msg_collection(&snac, id, total);
4051 }
4052 else
4053 if (strcmp(p_path, "following") == 0) {
4054 int total = 0;
4055
4056 if (show_contact_metrics) {
4057 total = following_list_len(&snac);
4058 }
4059
4060 xs *id = xs_fmt("%s/%s", snac.actor, p_path);
4061 msg = msg_collection(&snac, id, total);
4062 }
4063 else
4064 if (xs_startswith(p_path, "p/")) {
4065 xs *id = xs_fmt("%s/%s", snac.actor, p_path);
4066
4067 status = object_get(id, &msg);
4068
4069 /* don't return non-public objects */
4070 if (valid_status(status) && !is_msg_public(msg))
4071 status = HTTP_STATUS_NOT_FOUND;
4072 }
4073 else
4074 if (xs_startswith(p_path, "r/")) {
4075 /* replies to a post */
4076 xs *s = xs_dup(p_path);
4077 s[0] = 'p';
4078
4079 xs *id = xs_fmt("%s/%s", snac.actor, s);
4080
4081 xs *obj = NULL;
4082 status = object_get(id, &obj);
4083
4084 /* don't return non-public objects */
4085 if (!valid_status(status))
4086 status = HTTP_STATUS_NOT_FOUND;
4087 else
4088 if (!is_msg_public(obj))
4089 status = HTTP_STATUS_NOT_FOUND;
4090 else
4091 if (xs_dict_get(q_vars, "page"))
4092 msg = msg_replies(&snac, id, 1);
4093 else {
4094 const xs_dict *replies = xs_dict_get(obj, "replies");
4095 if (xs_is_dict(replies)) {
4096 msg = xs_dup(replies);
4097 msg = xs_dict_set(msg, "@context", "https:/""/www.w3.org/ns/activitystreams");
4098 }
4099 }
4100 }
4101 else
4102 status = HTTP_STATUS_NOT_FOUND;
4103
4104 if (status == HTTP_STATUS_OK && msg != NULL) {
4105 *body = xs_json_dumps(msg, 4);
4106 *b_size = strlen(*body);
4107 }
4108
4109 snac_debug(&snac, 1, xs_fmt("activitypub_get_handler serving %s %d", q_path, status));
4110
4111 user_free(&snac);
4112
4113 return status;
4114}
4115
4116
4117int activitypub_post_handler(const xs_dict *req, const char *q_path,
4118 char *payload, int p_size,
4119 char **body, int *b_size, char **ctype)
4120/* processes an input message */
4121{
4122 (void)b_size;
4123
4124 int status = HTTP_STATUS_ACCEPTED;
4125 const char *i_ctype = xs_dict_get(req, "content-type");
4126 snac snac;
4127 const char *v;
4128
4129 if (i_ctype == NULL) {
4130 *body = xs_str_new("no content-type");
4131 *ctype = "text/plain";
4132 return HTTP_STATUS_BAD_REQUEST;
4133 }
4134
4135 if (xs_is_null(payload)) {
4136 *body = xs_str_new("no payload");
4137 *ctype = "text/plain";
4138 return HTTP_STATUS_BAD_REQUEST;
4139 }
4140
4141 if (xs_str_in(i_ctype, "application/activity+json") == -1 &&
4142 xs_str_in(i_ctype, "application/ld+json") == -1)
4143 return 0;
4144
4145 if (p_size >= 100000) {
4146 *body = xs_str_new("too big");
4147 *ctype = "text/plain";
4148 return HTTP_STATUS_BAD_REQUEST;
4149 }
4150
4151 /* decode the message */
4152 xs *msg = xs_json_loads(payload);
4153 const char *id = xs_dict_get(msg, "id");
4154
4155 if (msg == NULL) {
4156 srv_log(xs_fmt("activitypub_post_handler JSON error %s", q_path));
4157
4158 srv_archive_error("activitypub_post_handler", "JSON error", req, payload);
4159
4160 *body = xs_str_new("JSON error");
4161 *ctype = "text/plain";
4162 return HTTP_STATUS_BAD_REQUEST;
4163 }
4164
4165 if (id && is_instance_blocked(id)) {
4166 srv_debug(1, xs_fmt("full instance block for %s", id));
4167
4168 *body = xs_str_new("blocked");
4169 *ctype = "text/plain";
4170 return HTTP_STATUS_FORBIDDEN;
4171 }
4172
4173 /* get the user and path */
4174 xs *l = xs_split_n(q_path, "/", 2);
4175
4176 if (xs_list_len(l) <= 1) {
4177 srv_debug(1, xs_fmt("activitypub_post_handler bad path %s", q_path));
4178 return HTTP_STATUS_BAD_REQUEST;
4179 }
4180
4181 if (xs_list_len(l) == 2 && strcmp(xs_list_get(l, 1), "shared-inbox") == 0) {
4182 enqueue_shared_input(msg, req, 0);
4183 return HTTP_STATUS_ACCEPTED;
4184 }
4185
4186 if (xs_list_len(l) != 3 || strcmp(xs_list_get(l, 2), "inbox") != 0) {
4187 /* strange q_path */
4188 srv_debug(1, xs_fmt("activitypub_post_handler unsupported path %s", q_path));
4189 return HTTP_STATUS_NOT_FOUND;
4190 }
4191
4192 const char *uid = xs_list_get(l, 1);
4193 if (!user_open(&snac, uid)) {
4194 /* invalid user */
4195 status = grave(uid, OP_CHECK) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND;
4196 srv_debug(1, xs_fmt("activitypub_post_handler bad user %s %d", uid, status));
4197 return status;
4198 }
4199
4200 /* if it has a digest, check it now, because
4201 later the payload won't be exactly the same */
4202 if ((v = xs_dict_get(req, "digest")) != NULL) {
4203 xs *s1 = xs_sha256_base64(payload, p_size);
4204 xs *s2 = xs_fmt("SHA-256=%s", s1);
4205
4206 if (strcmp(s2, v) != 0) {
4207 srv_log(xs_fmt("digest check FAILED"));
4208
4209 *body = xs_str_new("bad digest");
4210 *ctype = "text/plain";
4211 status = HTTP_STATUS_BAD_REQUEST;
4212 }
4213 }
4214
4215 /* if the message is from a muted actor, reject it right now */
4216 if (!xs_is_null(v = xs_dict_get(msg, "actor")) && *v) {
4217 if (is_muted(&snac, v)) {
4218 snac_log(&snac, xs_fmt("rejected message from MUTEd actor %s", v));
4219
4220 *body = xs_str_new("rejected");
4221 *ctype = "text/plain";
4222 status = HTTP_STATUS_FORBIDDEN;
4223 }
4224 }
4225
4226 if (valid_status(status)) {
4227 enqueue_input(&snac, msg, req, 0);
4228 *ctype = "application/activity+json";
4229 }
4230
4231 user_free(&snac);
4232
4233 return status;
4234}