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