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