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