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