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