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