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