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