2.92 mastoapi.c
   1/* snac - A simple, minimalistic ActivityPub instance */
   2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
   3
   4#ifndef NO_MASTODON_API
   5
   6#include "xs.h"
   7#include "xs_hex.h"
   8#include "xs_openssl.h"
   9#include "xs_json.h"
  10#include "xs_io.h"
  11#include "xs_time.h"
  12#include "xs_glob.h"
  13#include "xs_set.h"
  14#include "xs_random.h"
  15#include "xs_url.h"
  16#include "xs_mime.h"
  17#include "xs_match.h"
  18#include "xs_unicode.h"
  19#include "xs_http.h"
  20
  21#include "snac.h"
  22
  23#include <sys/time.h>
  24
  25static xs_str *random_str(void)
  26/* just what is says in the tin */
  27{
  28    unsigned int data[4] = {0};
  29
  30    xs_rnd_buf(data, sizeof(data));
  31    return xs_hex_enc((char *)data, sizeof(data));
  32}
  33
  34
  35int app_add(const char *id, const xs_dict *app)
  36/* stores an app */
  37{
  38    if (!xs_is_hex(id))
  39        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
  40
  41    int status = HTTP_STATUS_CREATED;
  42    xs *fn     = xs_fmt("%s/app/", srv_basedir);
  43    FILE *f;
  44
  45    mkdirx(fn);
  46    fn = xs_str_cat(fn, id);
  47    fn = xs_str_cat(fn, ".json");
  48
  49    if ((f = fopen(fn, "w")) != NULL) {
  50        xs_json_dump(app, 4, f);
  51        fclose(f);
  52    }
  53    else
  54        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
  55
  56    return status;
  57}
  58
  59
  60xs_str *_app_fn(const char *id)
  61{
  62    return xs_fmt("%s/app/%s.json", srv_basedir, id);
  63}
  64
  65
  66xs_dict *app_get(const char *id)
  67/* gets an app */
  68{
  69    if (!xs_is_hex(id))
  70        return NULL;
  71
  72    xs *fn       = _app_fn(id);
  73    xs_dict *app = NULL;
  74    FILE *f;
  75
  76    if ((f = fopen(fn, "r")) != NULL) {
  77        app = xs_json_load(f);
  78        fclose(f);
  79
  80    }
  81
  82    return app;
  83}
  84
  85
  86int app_del(const char *id)
  87/* deletes an app */
  88{
  89    if (!xs_is_hex(id))
  90        return -1;
  91
  92    xs *fn = _app_fn(id);
  93
  94    return unlink(fn);
  95}
  96
  97
  98int token_add(const char *id, const xs_dict *token)
  99/* stores a token */
 100{
 101    if (!xs_is_hex(id))
 102        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
 103
 104    int status = HTTP_STATUS_CREATED;
 105    xs *fn     = xs_fmt("%s/token/", srv_basedir);
 106    FILE *f;
 107
 108    mkdirx(fn);
 109    fn = xs_str_cat(fn, id);
 110    fn = xs_str_cat(fn, ".json");
 111
 112    if ((f = fopen(fn, "w")) != NULL) {
 113        xs_json_dump(token, 4, f);
 114        fclose(f);
 115    }
 116    else
 117        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 118
 119    return status;
 120}
 121
 122
 123xs_dict *token_get(const char *id)
 124/* gets a token */
 125{
 126    if (!xs_is_hex(id))
 127        return NULL;
 128
 129    xs *fn         = xs_fmt("%s/token/%s.json", srv_basedir, id);
 130    xs_dict *token = NULL;
 131    FILE *f;
 132
 133    if ((f = fopen(fn, "r")) != NULL) {
 134        token = xs_json_load(f);
 135        fclose(f);
 136
 137        /* 'touch' the file */
 138        utimes(fn, NULL);
 139
 140        /* also 'touch' the app */
 141        const char *app_id = xs_dict_get(token, "client_id");
 142
 143        if (app_id) {
 144            xs *afn = xs_fmt("%s/app/%s.json", srv_basedir, app_id);
 145            utimes(afn, NULL);
 146        }
 147    }
 148
 149    return token;
 150}
 151
 152
 153int token_del(const char *id)
 154/* deletes a token */
 155{
 156    if (!xs_is_hex(id))
 157        return -1;
 158
 159    xs *fn = xs_fmt("%s/token/%s.json", srv_basedir, id);
 160
 161    return unlink(fn);
 162}
 163
 164
 165const char * const login_page = ""
 166"<!DOCTYPE html>\n"
 167"<html>\n"
 168"<head>\n"
 169"<title>%s OAuth - Snac2</title>\n"
 170"<meta content=\"width=device-width, initial-scale=1, minimum-scale=1, user-scalable=no\" name=\"viewport\">"
 171"<style>:root {color-scheme: light dark}</style>\n"
 172"</head>\n"
 173"<body><h1>%s OAuth identify</h1>\n"
 174"<div style=\"background-color: red; color: white\">%s</div>\n"
 175"<form method=\"post\" action=\"%s:/" "/%s/%s\">\n"
 176"<p>Login: <input type=\"text\" name=\"login\" autocapitalize=\"off\"></p>\n"
 177"<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
 178"<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
 179"<input type=\"hidden\" name=\"cid\" value=\"%s\">\n"
 180"<input type=\"hidden\" name=\"state\" value=\"%s\">\n"
 181"<input type=\"submit\" value=\"OK\">\n"
 182"</form><p>%s</p></body></html>\n"
 183"";
 184
 185int oauth_get_handler(const xs_dict *req, const char *q_path,
 186                      char **body, int *b_size, char **ctype)
 187{
 188    (void)b_size;
 189
 190    if (!xs_startswith(q_path, "/oauth/"))
 191        return 0;
 192
 193    int status   = HTTP_STATUS_NOT_FOUND;
 194    const xs_dict *msg = xs_dict_get(req, "q_vars");
 195    xs *cmd      = xs_replace_n(q_path, "/oauth", "", 1);
 196
 197    srv_debug(1, xs_fmt("oauth_get_handler %s", q_path));
 198
 199    if (strcmp(cmd, "/authorize") == 0) { /** **/
 200        const char *cid   = xs_dict_get(msg, "client_id");
 201        const char *ruri  = xs_dict_get(msg, "redirect_uri");
 202        const char *rtype = xs_dict_get(msg, "response_type");
 203        const char *state = xs_dict_get(msg, "state");
 204
 205        status = HTTP_STATUS_BAD_REQUEST;
 206
 207        if (cid && ruri && rtype && strcmp(rtype, "code") == 0) {
 208            xs *app = app_get(cid);
 209
 210            if (app != NULL) {
 211                const char *host = xs_dict_get(srv_config, "host");
 212                const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 213
 214                if (xs_is_null(state))
 215                    state = "";
 216
 217                *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-login",
 218                                ruri, cid, state, USER_AGENT);
 219                *ctype = "text/html";
 220                status = HTTP_STATUS_OK;
 221
 222                srv_debug(1, xs_fmt("oauth authorize: generating login page"));
 223            }
 224            else
 225                srv_debug(1, xs_fmt("oauth authorize: bad client_id %s", cid));
 226        }
 227        else
 228            srv_debug(1, xs_fmt("oauth authorize: invalid or unset arguments"));
 229    }
 230    else
 231    if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
 232        const char *host = xs_dict_get(srv_config, "host");
 233        const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 234
 235        *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-get-token",
 236                        "", "", "", USER_AGENT);
 237        *ctype = "text/html";
 238        status = HTTP_STATUS_OK;
 239
 240    }
 241
 242    return status;
 243}
 244
 245
 246int oauth_post_handler(const xs_dict *req, const char *q_path,
 247                       const char *payload, int p_size,
 248                       char **body, int *b_size, char **ctype)
 249{
 250    (void)p_size;
 251    (void)b_size;
 252
 253    if (!xs_startswith(q_path, "/oauth/"))
 254        return 0;
 255
 256    int status   = HTTP_STATUS_NOT_FOUND;
 257
 258    const char *i_ctype = xs_dict_get(req, "content-type");
 259    xs *args      = NULL;
 260
 261    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
 262        if (!xs_is_null(payload))
 263            args = xs_json_loads(payload);
 264    }
 265    else
 266    if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded") && payload) {
 267        args    = xs_url_vars(payload);
 268    }
 269    else
 270        args = xs_dup(xs_dict_get(req, "p_vars"));
 271
 272    if (args == NULL)
 273        return HTTP_STATUS_BAD_REQUEST;
 274
 275    xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
 276
 277    srv_debug(1, xs_fmt("oauth_post_handler %s", q_path));
 278
 279    if (strcmp(cmd, "/x-snac-login") == 0) { /** **/
 280        const char *login  = xs_dict_get(args, "login");
 281        const char *passwd = xs_dict_get(args, "passwd");
 282        const char *redir  = xs_dict_get(args, "redir");
 283        const char *cid    = xs_dict_get(args, "cid");
 284        const char *state  = xs_dict_get(args, "state");
 285        const char *host   = xs_dict_get(srv_config, "host");
 286        const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 287
 288        /* by default, generate another login form with an error */
 289        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-login",
 290                        redir, cid, state, USER_AGENT);
 291        *ctype = "text/html";
 292        status = HTTP_STATUS_OK;
 293
 294        if (login && passwd && redir && cid) {
 295            snac snac;
 296
 297            if (user_open(&snac, login)) {
 298                const char *addr = xs_or(xs_dict_get(req, "remote-addr"),
 299                                         xs_dict_get(req, "x-forwarded-for"));
 300
 301                if (badlogin_check(login, addr)) {
 302                    /* check the login + password */
 303                    if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
 304                        /* success! redirect to the desired uri */
 305                        xs *code = random_str();
 306
 307                        xs_free(*body);
 308
 309                        if (strcmp(redir, "urn:ietf:wg:oauth:2.0:oob") == 0) {
 310                            *body = xs_dup(code);
 311                        }
 312                        else {
 313                            if (xs_str_in(redir, "?") != -1)
 314                                *body = xs_fmt("%s&code=%s", redir, code);
 315                            else
 316                                *body = xs_fmt("%s?code=%s", redir, code);
 317
 318                            status = HTTP_STATUS_SEE_OTHER;
 319                        }
 320
 321                        /* if there is a state, add it */
 322                        if (!xs_is_null(state) && *state) {
 323                            *body = xs_str_cat(*body, "&state=");
 324                            *body = xs_str_cat(*body, state);
 325                        }
 326
 327                        srv_log(xs_fmt("oauth x-snac-login: '%s' success, redirect to %s",
 328                                   login, *body));
 329
 330                        /* assign the login to the app */
 331                        xs *app = app_get(cid);
 332
 333                        if (app != NULL) {
 334                            app = xs_dict_set(app, "uid",  login);
 335                            app = xs_dict_set(app, "code", code);
 336                            app_add(cid, app);
 337                        }
 338                        else
 339                            srv_log(xs_fmt("oauth x-snac-login: error getting app %s", cid));
 340                    }
 341                    else {
 342                        srv_debug(1, xs_fmt("oauth x-snac-login: login '%s' incorrect", login));
 343                        badlogin_inc(login, addr);
 344                    }
 345                }
 346
 347                user_free(&snac);
 348            }
 349            else
 350                srv_debug(1, xs_fmt("oauth x-snac-login: bad user '%s'", login));
 351        }
 352        else
 353            srv_debug(1, xs_fmt("oauth x-snac-login: invalid or unset arguments"));
 354    }
 355    else
 356    if (strcmp(cmd, "/token") == 0) { /** **/
 357        xs *wrk = NULL;
 358        const char *gtype = xs_dict_get(args, "grant_type");
 359        const char *code  = xs_dict_get(args, "code");
 360        const char *cid   = xs_dict_get(args, "client_id");
 361        const char *csec  = xs_dict_get(args, "client_secret");
 362        const char *ruri  = xs_dict_get(args, "redirect_uri");
 363        const char *scope = xs_dict_get(args, "scope");
 364
 365        /* no client_secret? check if it's inside an authorization header
 366           (AndStatus does it this way) */
 367        if (xs_is_null(csec)) {
 368            const char *auhdr = xs_dict_get(req, "authorization");
 369
 370            if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) {
 371                xs *s1 = xs_replace_n(auhdr, "Basic ", "", 1);
 372                int size;
 373                xs *s2 = xs_base64_dec(s1, &size);
 374
 375                if (!xs_is_null(s2)) {
 376                    xs *l1 = xs_split(s2, ":");
 377
 378                    if (xs_list_len(l1) == 2) {
 379                        wrk = xs_dup(xs_list_get(l1, 1));
 380                        csec = wrk;
 381                    }
 382                }
 383            }
 384        }
 385
 386        /* no code?
 387           I'm not sure of the impacts of this right now, but Subway Tooter does not
 388           provide a code so one must be generated */
 389        if (xs_is_null(code)){
 390            code = random_str();
 391        }
 392        if (gtype && code && cid && csec && ruri) {
 393            xs *app = app_get(cid);
 394
 395            if (app == NULL) {
 396                status = HTTP_STATUS_UNAUTHORIZED;
 397                srv_log(xs_fmt("oauth token: invalid app %s", cid));
 398            }
 399            else
 400            if (strcmp(csec, xs_dict_get(app, "client_secret")) != 0) {
 401                status = HTTP_STATUS_UNAUTHORIZED;
 402                srv_log(xs_fmt("oauth token: invalid client_secret for app %s", cid));
 403            }
 404            else {
 405                xs *rsp   = xs_dict_new();
 406                xs *cat   = xs_number_new(time(NULL));
 407                xs *tokid = random_str();
 408
 409                rsp = xs_dict_append(rsp, "access_token", tokid);
 410                rsp = xs_dict_append(rsp, "token_type",   "Bearer");
 411                rsp = xs_dict_append(rsp, "created_at",   cat);
 412
 413                if (!xs_is_null(scope))
 414                    rsp = xs_dict_append(rsp, "scope", scope);
 415                else
 416                    rsp = xs_dict_append(rsp, "scope", "");
 417
 418                *body  = xs_json_dumps(rsp, 4);
 419                *ctype = "application/json";
 420                status = HTTP_STATUS_OK;
 421
 422                const char *uid = xs_dict_get(app, "uid");
 423
 424                srv_debug(1, xs_fmt("oauth token: "
 425                                "successful login for %s, new token %s", uid, tokid));
 426
 427                xs *token = xs_dict_new();
 428                token = xs_dict_append(token, "token",         tokid);
 429                token = xs_dict_append(token, "client_id",     cid);
 430                token = xs_dict_append(token, "client_secret", csec);
 431                token = xs_dict_append(token, "uid",           uid);
 432                token = xs_dict_append(token, "code",          code);
 433
 434                token_add(tokid, token);
 435            }
 436        }
 437        else {
 438            srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
 439            status = HTTP_STATUS_BAD_REQUEST;
 440        }
 441    }
 442    else
 443    if (strcmp(cmd, "/revoke") == 0) { /** **/
 444        const char *cid   = xs_dict_get(args, "client_id");
 445        const char *csec  = xs_dict_get(args, "client_secret");
 446        const char *tokid = xs_dict_get(args, "token");
 447
 448        if (cid && csec && tokid) {
 449            xs *token = token_get(tokid);
 450
 451            *body  = xs_str_new("{}");
 452            *ctype = "application/json";
 453
 454            if (token == NULL || strcmp(csec, xs_dict_get(token, "client_secret")) != 0) {
 455                srv_debug(1, xs_fmt("oauth revoke: bad secret for token %s", tokid));
 456                status = HTTP_STATUS_FORBIDDEN;
 457            }
 458            else {
 459                token_del(tokid);
 460                srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
 461                status = HTTP_STATUS_OK;
 462
 463                /* also delete the app, as it serves no purpose from now on */
 464                app_del(cid);
 465            }
 466        }
 467        else {
 468            srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
 469            status = HTTP_STATUS_FORBIDDEN;
 470        }
 471    }
 472    if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
 473        const char *login  = xs_dict_get(args, "login");
 474        const char *passwd = xs_dict_get(args, "passwd");
 475        const char *host   = xs_dict_get(srv_config, "host");
 476        const char *proto  = xs_dict_get_def(srv_config, "protocol", "https");
 477
 478        /* by default, generate another login form with an error */
 479        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-get-token",
 480                        "", "", "", USER_AGENT);
 481        *ctype = "text/html";
 482        status = HTTP_STATUS_OK;
 483
 484        if (login && passwd) {
 485            snac user;
 486
 487            if (user_open(&user, login)) {
 488                const char *addr = xs_or(xs_dict_get(req, "remote-addr"),
 489                                         xs_dict_get(req, "x-forwarded-for"));
 490
 491                if (badlogin_check(login, addr)) {
 492                    /* check the login + password */
 493                    if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
 494                        /* success! create a new token */
 495                        xs *tokid = random_str();
 496
 497                        srv_debug(1, xs_fmt("x-snac-new-token: "
 498                                    "successful login for %s, new token %s", login, tokid));
 499
 500                        xs *token = xs_dict_new();
 501                        token = xs_dict_append(token, "token",         tokid);
 502                        token = xs_dict_append(token, "client_id",     "snac-client");
 503                        token = xs_dict_append(token, "client_secret", "");
 504                        token = xs_dict_append(token, "uid",           login);
 505                        token = xs_dict_append(token, "code",          "");
 506
 507                        token_add(tokid, token);
 508
 509                        *ctype = "text/plain";
 510                        xs_free(*body);
 511                        *body = xs_dup(tokid);
 512                    }
 513                    else
 514                        badlogin_inc(login, addr);
 515
 516                    user_free(&user);
 517                }
 518            }
 519        }
 520    }
 521
 522    return status;
 523}
 524
 525
 526xs_str *mastoapi_id(const xs_dict *msg)
 527/* returns a somewhat Mastodon-compatible status id */
 528{
 529    const char *id = xs_dict_get(msg, "id");
 530    xs *md5        = xs_md5_hex(id, strlen(id));
 531
 532    return xs_fmt("%10.0f%s", object_ctime_by_md5(md5), md5);
 533}
 534
 535#define MID_TO_MD5(id) (id + 10)
 536
 537
 538static xs_val *get_count_from_actor(const xs_dict *actor, const char *field_names[], const char *collection_url_field)
 539/* helper to extract count from actor dict using various field name variations or from cached collection */
 540{
 541    xs_val *count = NULL;
 542
 543    /* try direct field name variations first */
 544    for (int i = 0; field_names[i] && xs_type(count) != XSTYPE_NUMBER; i++) {
 545        const xs_number *val = xs_dict_get(actor, field_names[i]);
 546        if (xs_type(val) == XSTYPE_NUMBER)
 547            count = xs_dup(val);
 548    }
 549
 550    /* if not found directly, try to get from cached collection object */
 551    if (xs_type(count) != XSTYPE_NUMBER) {
 552        const char *url = xs_dict_get(actor, collection_url_field);
 553        if (!xs_is_null(url)) {
 554            xs *coll = NULL;
 555            if (valid_status(object_get(url, &coll))) {
 556                const xs_number *total = xs_dict_get(coll, "totalItems");
 557                if (xs_type(total) == XSTYPE_NUMBER)
 558                    count = xs_dup(total);
 559            }
 560        }
 561    }
 562
 563    return count;
 564}
 565
 566
 567static const xs_list *get_collection_items(snac *snac, const char *collection_url,
 568                                           xs_dict **out_collection, xs_dict **out_page)
 569/* fetches items from an ActivityPub collection (outbox, etc) with minimal HTTP requests */
 570{
 571    const xs_list *items = NULL;
 572    xs_dict *collection = NULL;
 573
 574    if (valid_status(activitypub_request(snac, collection_url, &collection))) {
 575        /* check if items are directly embedded */
 576        items = xs_dict_get(collection, "orderedItems");
 577        if (xs_is_null(items))
 578            items = xs_dict_get(collection, "items");
 579
 580        if (!xs_is_null(items)) {
 581            /* items found in main collection - transfer ownership to keep items valid */
 582            if (out_collection)
 583                *out_collection = collection;
 584            return items;
 585        }
 586
 587        /* if no items, try fetching first page (only 1 extra request) */
 588        const char *first_url = xs_dict_get(collection, "first");
 589        if (!xs_is_null(first_url)) {
 590            xs_dict *first_page = NULL;
 591            if (valid_status(activitypub_request(snac, first_url, &first_page))) {
 592                items = xs_dict_get(first_page, "orderedItems");
 593                if (xs_is_null(items))
 594                    items = xs_dict_get(first_page, "items");
 595
 596                if (!xs_is_null(items)) {
 597                    /* items found in first page - transfer ownership to keep items valid */
 598                    if (out_page)
 599                        *out_page = first_page;
 600                    xs_free(collection);
 601                    return items;
 602                }
 603                xs_free(first_page);
 604            }
 605        }
 606        xs_free(collection);
 607    }
 608
 609    return NULL;
 610}
 611
 612
 613static const xs_dict *extract_post_from_item(const xs_val *item)
 614/* extracts the post object from an outbox item, handling Create/Announce wrappers */
 615{
 616    if (xs_type(item) != XSTYPE_DICT)
 617        return NULL;
 618
 619    const char *item_type = xs_dict_get(item, "type");
 620    const xs_dict *post = item;
 621
 622    /* if it's an activity, try to get embedded object */
 623    if (!xs_is_null(item_type) &&
 624        (strcmp(item_type, "Create") == 0 || strcmp(item_type, "Announce") == 0)) {
 625        const xs_val *obj = xs_dict_get(item, "object");
 626
 627        /* only use embedded objects, skip URL references (would need HTTP fetch) */
 628        if (!xs_is_null(obj) && xs_type(obj) == XSTYPE_DICT)
 629            post = obj;
 630        else
 631            return NULL;
 632    }
 633
 634    return post;
 635}
 636
 637
 638static int is_valid_post_type(const char *post_type)
 639/* checks if a type is a valid post type for timeline display */
 640{
 641    return !xs_is_null(post_type) &&
 642           (strcmp(post_type, "Note") == 0 ||
 643            strcmp(post_type, "Article") == 0 ||
 644            strcmp(post_type, "Question") == 0 ||
 645            strcmp(post_type, "Page") == 0);
 646}
 647
 648
 649xs_dict *mastoapi_account(snac *logged, const xs_dict *actor)
 650/* converts an ActivityPub actor to a Mastodon account */
 651{
 652    const char *id  = xs_dict_get(actor, "id");
 653    const char *pub = xs_dict_get(actor, "published");
 654    const char *proxy = NULL;
 655
 656    if (xs_type(id) != XSTYPE_STRING)
 657        return NULL;
 658
 659    if (logged && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
 660        proxy = logged->actor;
 661
 662    const char *prefu = xs_dict_get(actor, "preferredUsername");
 663
 664    const char *display_name = xs_dict_get(actor, "name");
 665    if (xs_is_null(display_name) || *display_name == '\0')
 666        display_name = prefu;
 667
 668    xs_dict *acct = xs_dict_new();
 669    xs *acct_md5  = xs_md5_hex(id, strlen(id));
 670    acct = xs_dict_append(acct, "id",           acct_md5);
 671    acct = xs_dict_append(acct, "username",     prefu);
 672    acct = xs_dict_append(acct, "display_name", display_name);
 673    acct = xs_dict_append(acct, "discoverable", xs_stock(XSTYPE_TRUE));
 674    acct = xs_dict_append(acct, "group",        xs_stock(XSTYPE_FALSE));
 675    acct = xs_dict_append(acct, "hide_collections", xs_stock(XSTYPE_FALSE));
 676    acct = xs_dict_append(acct, "indexable",    xs_stock(XSTYPE_TRUE));
 677    acct = xs_dict_append(acct, "noindex",      xs_stock(XSTYPE_FALSE));
 678    acct = xs_dict_append(acct, "roles",        xs_stock(XSTYPE_LIST));
 679
 680    {
 681        /* create the acct field as user@host */
 682        xs *l     = xs_split(id, "/");
 683        xs *fquid = xs_fmt("%s@%s", prefu, xs_list_get(l, 2));
 684        acct      = xs_dict_append(acct, "acct", fquid);
 685    }
 686
 687    if (pub)
 688        acct = xs_dict_append(acct, "created_at", pub);
 689    else {
 690        /* unset created_at crashes Tusky, so lie like a mf */
 691        xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
 692        acct = xs_dict_append(acct, "created_at", date);
 693    }
 694
 695    xs *last_status_at = xs_str_utctime(0, "%Y-%m-%d");
 696    acct = xs_dict_append(acct, "last_status_at", last_status_at);
 697
 698    const char *note = xs_dict_get(actor, "summary");
 699    if (xs_is_null(note))
 700        note = "";
 701
 702    if (strcmp(xs_dict_get(actor, "type"), "Service") == 0)
 703        acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_TRUE));
 704    else
 705        acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_FALSE));
 706
 707    acct = xs_dict_append(acct, "note", note);
 708
 709    acct = xs_dict_append(acct, "url", id);
 710    acct = xs_dict_append(acct, "uri", id);
 711
 712    xs *avatar  = NULL;
 713    const xs_dict *av = xs_dict_get(actor, "icon");
 714
 715    if (xs_type(av) == XSTYPE_DICT) {
 716        const char *url = xs_dict_get(av, "url");
 717
 718        if (url != NULL)
 719            avatar = make_url(url, proxy, 1);
 720    }
 721
 722    if (avatar == NULL)
 723        avatar = xs_fmt("%s/susie.png", srv_baseurl);
 724
 725    acct = xs_dict_append(acct, "avatar", avatar);
 726    acct = xs_dict_append(acct, "avatar_static", avatar);
 727
 728    xs *header  = NULL;
 729    const xs_dict *hd = xs_dict_get(actor, "image");
 730
 731    if (xs_type(hd) == XSTYPE_DICT)
 732        header = make_url(xs_dict_get(hd, "url"), proxy, 1);
 733
 734    if (xs_is_null(header))
 735        header = xs_str_new(NULL);
 736
 737    acct = xs_dict_append(acct, "header", header);
 738    acct = xs_dict_append(acct, "header_static", header);
 739
 740    /* emojis */
 741    const xs_list *p;
 742    if (!xs_is_null(p = xs_dict_get(actor, "tag"))) {
 743        xs *eml = xs_list_new();
 744        const xs_dict *v;
 745        int c = 0;
 746
 747        while (xs_list_next(p, &v, &c)) {
 748            const char *type = xs_dict_get(v, "type");
 749
 750            if (!xs_is_null(type) && strcmp(type, "Emoji") == 0) {
 751                const char *name    = xs_dict_get(v, "name");
 752                const xs_dict *icon = xs_dict_get(v, "icon");
 753
 754                if (!xs_is_null(name) && !xs_is_null(icon)) {
 755                    const char *o_url = xs_dict_get(icon, "url");
 756
 757                    if (!xs_is_null(o_url)) {
 758                        xs *url = make_url(o_url, proxy, 1);
 759                        xs *nm = xs_strip_chars_i(xs_dup(name), ":");
 760                        xs *d1 = xs_dict_new();
 761
 762                        d1 = xs_dict_append(d1, "shortcode",         nm);
 763                        d1 = xs_dict_append(d1, "url",               url);
 764                        d1 = xs_dict_append(d1, "static_url",        url);
 765                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
 766
 767                        eml = xs_list_append(eml, d1);
 768                    }
 769                }
 770            }
 771        }
 772
 773        acct = xs_dict_append(acct, "emojis", eml);
 774    }
 775
 776    acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
 777
 778    /* try to get counts from actor object if available (some servers include these) */
 779    const char *fcount_fields[] = { "followersCount", "followers_count", NULL };
 780    const char *gcount_fields[] = { "followingCount", "following_count", NULL };
 781    const char *scount_fields[] = { "statusesCount", "statuses_count", "totalItems", NULL };
 782
 783    xs *followers_count = get_count_from_actor(actor, fcount_fields, "followers");
 784    xs *following_count = get_count_from_actor(actor, gcount_fields, "following");
 785    xs *statuses_count = get_count_from_actor(actor, scount_fields, "outbox");
 786
 787    acct = xs_dict_append(acct, "followers_count",
 788        xs_type(followers_count) == XSTYPE_NUMBER ? followers_count : xs_stock(0));
 789    acct = xs_dict_append(acct, "following_count",
 790        xs_type(following_count) == XSTYPE_NUMBER ? following_count : xs_stock(0));
 791    acct = xs_dict_append(acct, "statuses_count",
 792        xs_type(statuses_count) == XSTYPE_NUMBER ? statuses_count : xs_stock(0));
 793
 794    xs *fields = xs_list_new();
 795    p = xs_dict_get(actor, "attachment");
 796    const xs_dict *v;
 797
 798    /* dict of validated links */
 799    xs_dict *val_links = NULL;
 800    const xs_dict *metadata  = xs_stock(XSTYPE_DICT);
 801    snac user = {0};
 802
 803    if (xs_startswith(id, srv_baseurl)) {
 804        /* if it's a local user, open it and pick its validated links */
 805        if (user_open(&user, prefu)) {
 806            val_links = user.links;
 807            metadata  = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT));
 808
 809            /* does this user want to publish their contact metrics? */
 810            if (xs_is_true(xs_dict_get(user.config, "show_contact_metrics"))) {
 811                int fwing = following_list_len(&user);
 812                int fwers = follower_list_len(&user);
 813                xs *ni = xs_number_new(fwing);
 814                xs *ne = xs_number_new(fwers);
 815
 816                acct = xs_dict_append(acct, "followers_count", ne);
 817                acct = xs_dict_append(acct, "following_count", ni);
 818            }
 819        }
 820    }
 821
 822    if (xs_is_null(val_links))
 823        val_links = xs_stock(XSTYPE_DICT);
 824
 825    int c = 0;
 826    while (xs_list_next(p, &v, &c)) {
 827        const char *type  = xs_dict_get(v, "type");
 828        const char *name  = xs_dict_get(v, "name");
 829        const char *value = xs_dict_get(v, "value");
 830
 831        if (!xs_is_null(type) && !xs_is_null(name) &&
 832            !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) {
 833            xs *val_date = NULL;
 834
 835            const char *url = xs_dict_get(metadata, name);
 836
 837            if (!xs_is_null(url) && xs_startswith(url, "https:/" "/")) {
 838                const xs_number *verified_time = xs_dict_get(val_links, url);
 839                if (xs_type(verified_time) == XSTYPE_NUMBER) {
 840                    time_t t = xs_number_get(verified_time);
 841
 842                    if (t > 0)
 843                        val_date = xs_str_utctime(t, ISO_DATE_SPEC);
 844                }
 845            }
 846
 847            xs *d = xs_dict_new();
 848
 849            d = xs_dict_append(d, "name", name);
 850            d = xs_dict_append(d, "value", value);
 851            d = xs_dict_append(d, "verified_at",
 852                xs_type(val_date) == XSTYPE_STRING && *val_date ?
 853                    val_date : xs_stock(XSTYPE_NULL));
 854
 855            fields = xs_list_append(fields, d);
 856        }
 857    }
 858
 859    user_free(&user);
 860
 861    acct = xs_dict_append(acct, "fields", fields);
 862
 863    return acct;
 864}
 865
 866
 867xs_str *mastoapi_date(const char *date)
 868/* converts an ISO 8601 date to whatever format Mastodon uses */
 869{
 870    xs_str *s = xs_crop_i(xs_dup(date), 0, 19);
 871    s = xs_str_cat(s, ".000Z");
 872
 873    return s;
 874}
 875
 876
 877xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg)
 878/* creates a mastoapi Poll object */
 879{
 880    xs_dict *poll = xs_dict_new();
 881    xs *mid       = mastoapi_id(msg);
 882    const xs_list *opts = NULL;
 883    const xs_val *v;
 884    int num_votes = 0;
 885    xs *options = xs_list_new();
 886
 887    poll = xs_dict_append(poll, "id", mid);
 888    const char *date = xs_dict_get(msg, "endTime");
 889    if (date == NULL)
 890        date = xs_dict_get(msg, "closed");
 891    if (date == NULL)
 892        return NULL;
 893
 894    xs *fd = mastoapi_date(date);
 895    poll = xs_dict_append(poll, "expires_at", fd);
 896
 897    date = xs_dict_get(msg, "closed");
 898    time_t t = 0;
 899
 900    if (date != NULL)
 901        t = xs_parse_iso_date(date, 0);
 902
 903    poll = xs_dict_append(poll, "expired",
 904            t < time(NULL) ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE));
 905
 906    if ((opts = xs_dict_get(msg, "oneOf")) != NULL)
 907        poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_FALSE));
 908    else {
 909        opts = xs_dict_get(msg, "anyOf");
 910        poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_TRUE));
 911    }
 912
 913    int c = 0;
 914    while (xs_list_next(opts, &v, &c)) {
 915        const char *title   = xs_dict_get(v, "name");
 916        const char *replies = xs_dict_get(v, "replies");
 917
 918        if (title && replies) {
 919            const char *votes_count = xs_dict_get(replies, "totalItems");
 920
 921            if (xs_type(votes_count) == XSTYPE_NUMBER) {
 922                xs *d = xs_dict_new();
 923                d = xs_dict_append(d, "title",  title);
 924                d = xs_dict_append(d, "votes_count", votes_count);
 925
 926                options = xs_list_append(options, d);
 927                num_votes += xs_number_get(votes_count);
 928            }
 929        }
 930    }
 931
 932    poll = xs_dict_append(poll, "options", options);
 933    xs *vc = xs_number_new(num_votes);
 934    poll = xs_dict_append(poll, "votes_count", vc);
 935
 936    poll = xs_dict_append(poll, "emojis", xs_stock(XSTYPE_LIST));
 937
 938    poll = xs_dict_append(poll, "voted",
 939            (snac && was_question_voted(snac, xs_dict_get(msg, "id"))) ?
 940                xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
 941
 942    return poll;
 943}
 944
 945
 946xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
 947/* converts an ActivityPub note to a Mastodon status */
 948{
 949    xs *actor = NULL;
 950    actor_get_refresh(snac, get_atto(msg), &actor);
 951    const char *proxy = NULL;
 952
 953    /* if the author is not here, discard */
 954    if (actor == NULL)
 955        return NULL;
 956
 957    if (snac && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
 958        proxy = snac->actor;
 959
 960    const char *type = xs_dict_get(msg, "type");
 961    const char *id   = xs_dict_get(msg, "id");
 962
 963    /* fail if it's not a valid actor */
 964    if (xs_is_null(type) || xs_is_null(id))
 965        return NULL;
 966
 967    xs *acct = mastoapi_account(snac, actor);
 968    if (acct == NULL)
 969        return NULL;
 970
 971    xs *idx = NULL;
 972    xs *ixc = NULL;
 973    const char *tmp;
 974    xs *mid  = mastoapi_id(msg);
 975
 976    xs_dict *st = xs_dict_new();
 977
 978    st = xs_dict_append(st, "id",           mid);
 979    st = xs_dict_append(st, "uri",          id);
 980    st = xs_dict_append(st, "url",          id);
 981    st = xs_dict_append(st, "account",      acct);
 982
 983    const char *published = xs_dict_get(msg, "published");
 984    xs *fd = NULL;
 985
 986    if (published)
 987        fd = mastoapi_date(published);
 988    else {
 989        xs *p = xs_str_iso_date(0);
 990        fd = mastoapi_date(p);
 991    }
 992
 993    st = xs_dict_append(st, "created_at", fd);
 994
 995    {
 996        const char *content = xs_dict_get(msg, "content");
 997        const char *name    = xs_dict_get(msg, "name");
 998        xs *s1 = NULL;
 999
1000        if (name && content)
1001            s1 = xs_fmt("%s<br><br>%s", name, content);
1002        else
1003        if (name)
1004            s1 = xs_dup(name);
1005        else
1006        if (content)
1007            s1 = xs_dup(content);
1008        else
1009            s1 = xs_str_new(NULL);
1010
1011        st = xs_dict_append(st, "content", s1);
1012    }
1013
1014    /* determine visibility: https://docs.joinmastodon.org/entities/Status/#visibility */
1015    const int scope = get_msg_visibility(msg);
1016    if (scope == SCOPE_PUBLIC)
1017        st = xs_dict_append(st, "visibility", "public");
1018    else if (scope == SCOPE_FOLLOWERS)
1019        st = xs_dict_append(st, "visibility", "private");
1020    else if (scope == SCOPE_MENTIONED)
1021        st = xs_dict_append(st, "visibility", "direct");
1022    else if (scope == SCOPE_UNLISTED)
1023        st = xs_dict_append(st, "visibility", "unlisted");
1024
1025    tmp = xs_dict_get(msg, "sensitive");
1026    if (xs_is_null(tmp))
1027        tmp = xs_stock(XSTYPE_FALSE);
1028
1029    st = xs_dict_append(st, "sensitive",    tmp);
1030
1031    tmp = xs_dict_get(msg, "summary");
1032    if (xs_is_null(tmp))
1033        tmp = "";
1034
1035    st = xs_dict_append(st, "spoiler_text", tmp);
1036
1037    /* create the list of attachments */
1038    xs *attach = get_attachments(msg);
1039
1040    {
1041        xs_list *p = attach;
1042        const xs_dict *v;
1043
1044        xs *matt = xs_list_new();
1045
1046        while (xs_list_iter(&p, &v)) {
1047            const char *type = xs_dict_get(v, "type");
1048            const char *o_href = xs_dict_get(v, "href");
1049            const char *name = xs_dict_get(v, "name");
1050
1051            if (xs_match(type, "image/*|video/*|audio/*|Image|Video")) { /* */
1052                xs *matteid = xs_fmt("%s_%d", id, xs_list_len(matt));
1053                xs *href = make_url(o_href, proxy, 1);
1054
1055                xs *d = xs_dict_new();
1056
1057                d = xs_dict_append(d, "id",          matteid);
1058                d = xs_dict_append(d, "url",         href);
1059                d = xs_dict_append(d, "preview_url", href);
1060                d = xs_dict_append(d, "remote_url",  href);
1061                d = xs_dict_append(d, "description", name);
1062
1063                d = xs_dict_append(d, "type", (*type == 'v' || *type == 'V') ? "video" :
1064                                              (*type == 'a' || *type == 'A') ? "audio" : "image");
1065
1066                matt = xs_list_append(matt, d);
1067            }
1068        }
1069
1070        st = xs_dict_append(st, "media_attachments", matt);
1071    }
1072
1073    {
1074        xs *ml  = xs_list_new();
1075        xs *htl = xs_list_new();
1076        xs *eml = xs_list_new();
1077        const xs_list *tag = xs_dict_get(msg, "tag");
1078        int n = 0;
1079
1080        xs *tag_list = NULL;
1081
1082        if (xs_type(tag) == XSTYPE_DICT) {
1083            tag_list = xs_list_new();
1084            tag_list = xs_list_append(tag_list, tag);
1085        }
1086        else
1087        if (xs_type(tag) == XSTYPE_LIST)
1088            tag_list = xs_dup(tag);
1089        else
1090            tag_list = xs_list_new();
1091
1092        tag = tag_list;
1093        const xs_dict *v;
1094
1095        int c = 0;
1096        while (xs_list_next(tag, &v, &c)) {
1097            const char *type = xs_dict_get(v, "type");
1098
1099            if (xs_is_null(type))
1100                continue;
1101
1102            xs *d1 = xs_dict_new();
1103
1104            if (strcmp(type, "Mention") == 0) {
1105                const char *name = xs_dict_get(v, "name");
1106                const char *href = xs_dict_get(v, "href");
1107
1108                if (!xs_is_null(name) && !xs_is_null(href) &&
1109                    (snac == NULL || strcmp(href, snac->actor) != 0)) {
1110                    xs *nm = xs_strip_chars_i(xs_dup(name), "@");
1111
1112                    xs *id = xs_fmt("%d", n++);
1113                    d1 = xs_dict_append(d1, "id", id);
1114                    d1 = xs_dict_append(d1, "username", nm);
1115                    d1 = xs_dict_append(d1, "acct", nm);
1116                    d1 = xs_dict_append(d1, "url", href);
1117
1118                    ml = xs_list_append(ml, d1);
1119                }
1120            }
1121            else
1122            if (strcmp(type, "Hashtag") == 0) {
1123                const char *name = xs_dict_get(v, "name");
1124                const char *href = xs_dict_get(v, "href");
1125
1126                if (!xs_is_null(name) && !xs_is_null(href)) {
1127                    xs *nm = xs_strip_chars_i(xs_dup(name), "#");
1128
1129                    d1 = xs_dict_append(d1, "name", nm);
1130                    d1 = xs_dict_append(d1, "url", href);
1131
1132                    htl = xs_list_append(htl, d1);
1133                }
1134            }
1135            else
1136            if (strcmp(type, "Emoji") == 0) {
1137                const char *name    = xs_dict_get(v, "name");
1138                const xs_dict *icon = xs_dict_get(v, "icon");
1139
1140                if (!xs_is_null(name) && !xs_is_null(icon)) {
1141                    const char *o_url = xs_dict_get(icon, "url");
1142
1143                    if (!xs_is_null(o_url)) {
1144                        xs *url = make_url(o_url, snac ? snac->actor : NULL, 1);
1145                        xs *nm = xs_strip_chars_i(xs_dup(name), ":");
1146
1147                        d1 = xs_dict_append(d1, "shortcode", nm);
1148                        d1 = xs_dict_append(d1, "url", url);
1149                        d1 = xs_dict_append(d1, "static_url", url);
1150                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
1151                        d1 = xs_dict_append(d1, "category", "Emojis");
1152
1153                        eml = xs_list_append(eml, d1);
1154                    }
1155                }
1156            }
1157        }
1158
1159        st = xs_dict_append(st, "mentions", ml);
1160        st = xs_dict_append(st, "tags",     htl);
1161        st = xs_dict_append(st, "emojis",   eml);
1162    }
1163    {
1164        xs *rl   = object_get_emoji_reacts(id);
1165        xs *frl  = xs_list_new(); /* final */
1166        xs *sfrl = xs_dict_new(); /* seen */
1167        int c = 0;
1168        const char *v;
1169
1170        while (xs_list_next(rl, &v, &c)) {
1171            xs *msg = NULL;
1172            if (valid_status(object_get_by_md5(v, &msg))) {
1173                const char *content = xs_dict_get(msg, "content");
1174                const char *actor = xs_dict_get(msg, "actor");
1175                const xs_list *contentl = xs_dict_get(sfrl, content);
1176
1177                if ((snac && is_muted(snac, actor)) || is_instance_blocked(actor))
1178                    continue;
1179
1180                /* NOTE: idk when there are no actor, but i encountered that bug.
1181                 * Probably because of one of my previous attempts.
1182                 * Keeping this just in case, can remove later */
1183                const char *me = actor && snac && strcmp(actor, snac->actor) == 0 ?
1184                    xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE);
1185                int count = 1;
1186
1187                if (contentl) {
1188                    count = atoi(xs_list_get(contentl, 0)) + 1;
1189                    if (strncmp(xs_list_get(contentl, 1), xs_stock(XSTYPE_TRUE), 1) == 0)
1190                        me = xs_stock(XSTYPE_TRUE);
1191                }
1192
1193                xs *fl = xs_list_new();
1194                xs *c1 = xs_fmt("%d", count);
1195                fl = xs_list_append(fl, c1, me);
1196                sfrl = xs_dict_append(sfrl, content, fl);
1197            }
1198        }
1199
1200        c = 0;
1201
1202        while (xs_list_next(rl, &v, &c)) {
1203            xs *msg = NULL;
1204            if (valid_status(object_get_by_md5(v, &msg))) {
1205                xs *d1 = xs_dict_new();
1206
1207                const xs_dict *icon = xs_dict_get(xs_list_get(xs_dict_get(msg, "tag"), 0), "icon");
1208                const char *o_url = xs_dict_get(icon, "url");
1209                const char *name = xs_dict_get(msg, "content");
1210                const char *actor = xs_dict_get(msg, "actor");
1211
1212                xs *nm = xs_dup(name);
1213                xs *url = NULL;
1214
1215                if (!xs_is_null(o_url)) {
1216                    if (actor && snac && !strcmp(actor, snac->actor))
1217                        url = make_url(o_url, NULL, 1);
1218                    else
1219                        url = xs_dup(o_url);
1220                }
1221
1222                xs *accounts = xs_list_new();
1223                if (actor) {
1224                    xs *d2 = NULL;
1225                    if (valid_status(object_get(actor, &d2))) {
1226                        xs *e_acct = mastoapi_account(snac, d2);
1227                        accounts = xs_list_append(accounts, e_acct);
1228                    }
1229                }
1230
1231                const xs_list *item = xs_dict_get(sfrl, nm);
1232                const xs_str *nb = xs_list_get(item, 0);
1233                const xs_val *me = xs_list_get(item, 1);
1234                if (item == NULL)
1235                    continue;
1236
1237                if (nm && strcmp(nm, "")) {
1238                    if (url && strcmp(url, "")) {
1239                        d1 = xs_dict_append(d1, "name",              nm);
1240                        d1 = xs_dict_append(d1, "shortcode",         nm);
1241                        d1 = xs_dict_append(d1, "accounts",          accounts);
1242                        d1 = xs_dict_append(d1, "me",                me);
1243                        d1 = xs_dict_append(d1, "url",               url);
1244                        d1 = xs_dict_append(d1, "static_url",        url);
1245                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
1246                        d1 = xs_dict_append(d1, "count", nb);
1247                    } else {
1248                        d1 = xs_dict_append(d1, "name",              nm);
1249                        d1 = xs_dict_append(d1, "count",             nb);
1250                        d1 = xs_dict_append(d1, "me",                me);
1251                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
1252                    }
1253                    sfrl = xs_dict_del(sfrl, nm);
1254                    frl = xs_list_append(frl, d1);
1255                }
1256            }
1257        }
1258
1259        st = xs_dict_append(st, "reactions", frl);
1260    }
1261
1262    xs_free(idx);
1263    xs_free(ixc);
1264    idx = object_likes(id);
1265    ixc = xs_number_new(xs_list_len(idx));
1266
1267    st = xs_dict_append(st, "favourites_count", ixc);
1268    st = xs_dict_append(st, "favourited",
1269        (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1270
1271    xs_free(idx);
1272    xs_free(ixc);
1273    idx = object_announces(id);
1274    ixc = xs_number_new(xs_list_len(idx));
1275
1276    st = xs_dict_append(st, "reblogs_count", ixc);
1277    st = xs_dict_append(st, "reblogged",
1278        (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1279
1280    /* get the last person who boosted this */
1281    xs *boosted_by_md5 = NULL;
1282    if (xs_list_len(idx))
1283        boosted_by_md5 = xs_dup(xs_list_get(idx, -1));
1284
1285    xs_free(idx);
1286    xs_free(ixc);
1287    idx = object_children(id);
1288    ixc = xs_number_new(xs_list_len(idx));
1289
1290    st = xs_dict_append(st, "replies_count", ixc);
1291
1292    /* default in_reply_to values */
1293    st = xs_dict_append(st, "in_reply_to_id",         xs_stock(XSTYPE_NULL));
1294    st = xs_dict_append(st, "in_reply_to_account_id", xs_stock(XSTYPE_NULL));
1295
1296    tmp = get_in_reply_to(msg);
1297    if (!xs_is_null(tmp)) {
1298        xs *irto = NULL;
1299
1300        if (valid_status(object_get(tmp, &irto))) {
1301            xs *irt_mid = mastoapi_id(irto);
1302            st = xs_dict_set(st, "in_reply_to_id", irt_mid);
1303
1304            const char *at = NULL;
1305            if (!xs_is_null(at = get_atto(irto))) {
1306                xs *at_md5 = xs_md5_hex(at, strlen(at));
1307                st = xs_dict_set(st, "in_reply_to_account_id", at_md5);
1308            }
1309        }
1310    }
1311
1312    st = xs_dict_append(st, "reblog",   xs_stock(XSTYPE_NULL));
1313    st = xs_dict_append(st, "card",     xs_stock(XSTYPE_NULL));
1314    st = xs_dict_append(st, "language", "en");
1315
1316    st = xs_dict_append(st, "filtered", xs_stock(XSTYPE_LIST));
1317    st = xs_dict_append(st, "muted",    xs_stock(XSTYPE_FALSE));
1318
1319    tmp = xs_dict_get(msg, "sourceContent");
1320    if (xs_is_null(tmp))
1321        tmp = "";
1322
1323    st = xs_dict_append(st, "text", tmp);
1324
1325    tmp = xs_dict_get(msg, "updated");
1326    xs *fd2 = NULL;
1327    if (xs_is_null(tmp))
1328        tmp = xs_stock(XSTYPE_NULL);
1329    else {
1330        fd2 = mastoapi_date(tmp);
1331        tmp = fd2;
1332    }
1333
1334    st = xs_dict_append(st, "edited_at", tmp);
1335
1336    if (strcmp(type, "Question") == 0) {
1337        xs *poll = mastoapi_poll(snac, msg);
1338        st = xs_dict_append(st, "poll", poll);
1339    }
1340    else
1341        st = xs_dict_append(st, "poll", xs_stock(XSTYPE_NULL));
1342
1343    st = xs_dict_append(st, "bookmarked",
1344        (snac && is_bookmarked(snac, id)) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1345
1346    st = xs_dict_append(st, "pinned",
1347        (snac && is_pinned(snac, id)) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1348
1349    /* is it a boost? */
1350    if (!xs_is_null(boosted_by_md5)) {
1351        /* create a new dummy status, using st as the 'reblog' field */
1352        xs_dict *bst = xs_dup(st);
1353        xs *b_actor = NULL;
1354
1355        if (valid_status(object_get_by_md5(boosted_by_md5, &b_actor))) {
1356            xs *b_acct   = mastoapi_account(snac, b_actor);
1357            xs *fake_uri = NULL;
1358
1359            if (snac)
1360                fake_uri = xs_fmt("%s/d/%s/Announce", snac->actor, mid);
1361            else
1362                fake_uri = xs_fmt("%s#%s", srv_baseurl, mid);
1363
1364            bst = xs_dict_set(bst, "uri", fake_uri);
1365            bst = xs_dict_set(bst, "url", fake_uri);
1366            bst = xs_dict_set(bst, "account", b_acct);
1367            bst = xs_dict_set(bst, "content", "");
1368            bst = xs_dict_set(bst, "reblog", st);
1369
1370            xs *b_id = xs_toupper_i(xs_dup(xs_dict_get(st, "id")));
1371            bst = xs_dict_set(bst, "id", b_id);
1372
1373            xs_free(st);
1374            st = bst;
1375        }
1376        else
1377            xs_free(bst);
1378    }
1379
1380    return st;
1381}
1382
1383
1384xs_dict *mastoapi_relationship(snac *snac, const char *md5)
1385{
1386    xs_dict *rel = NULL;
1387    xs *actor_o  = NULL;
1388
1389    if (valid_status(object_get_by_md5(md5, &actor_o))) {
1390        rel = xs_dict_new();
1391
1392        const char *actor = xs_dict_get(actor_o, "id");
1393
1394        rel = xs_dict_append(rel, "id",                   md5);
1395        rel = xs_dict_append(rel, "following",
1396            following_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1397
1398        rel = xs_dict_append(rel, "showing_reblogs",      xs_stock(XSTYPE_TRUE));
1399        rel = xs_dict_append(rel, "notifying",            xs_stock(XSTYPE_FALSE));
1400        rel = xs_dict_append(rel, "followed_by",
1401            follower_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1402
1403        rel = xs_dict_append(rel, "blocking",
1404            is_muted(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1405
1406        rel = xs_dict_append(rel, "muting",               xs_stock(XSTYPE_FALSE));
1407        rel = xs_dict_append(rel, "muting_notifications", xs_stock(XSTYPE_FALSE));
1408        rel = xs_dict_append(rel, "requested",            xs_stock(XSTYPE_FALSE));
1409        rel = xs_dict_append(rel, "domain_blocking",      xs_stock(XSTYPE_FALSE));
1410        rel = xs_dict_append(rel, "endorsed",             xs_stock(XSTYPE_FALSE));
1411        rel = xs_dict_append(rel, "note",                 "");
1412    }
1413
1414    return rel;
1415}
1416
1417
1418int process_auth_token(snac *snac, const xs_dict *req)
1419/* processes an authorization token, if there is one */
1420{
1421    int logged_in = 0;
1422    const char *v;
1423
1424    /* if there is an authorization field, try to validate it */
1425    if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
1426        xs *tokid = xs_replace_n(v, "Bearer ", "", 1);
1427        xs *token = token_get(tokid);
1428
1429        if (token != NULL) {
1430            const char *uid = xs_dict_get(token, "uid");
1431
1432            if (!xs_is_null(uid) && user_open(snac, uid)) {
1433                logged_in = 1;
1434
1435                /* this counts as a 'login' */
1436                lastlog_write(snac, "mastoapi");
1437
1438                srv_debug(2, xs_fmt("mastoapi auth: valid token for user '%s'", uid));
1439            }
1440            else
1441                srv_log(xs_fmt("mastoapi auth: corrupted token '%s'", tokid));
1442        }
1443        else
1444            srv_log(xs_fmt("mastoapi auth: invalid token '%s'", tokid));
1445    }
1446
1447    return logged_in;
1448}
1449
1450
1451void credentials_get(char **body, char **ctype, int *status, snac snac)
1452{
1453    xs *acct = xs_dict_new();
1454
1455    const xs_val *bot = xs_dict_get(snac.config, "bot");
1456
1457    acct = xs_dict_append(acct, "id", snac.md5);
1458    acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
1459    acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
1460    acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name"));
1461    acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published"));
1462    acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac.config, "published"));
1463    acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
1464    acct = xs_dict_append(acct, "url", snac.actor);
1465    acct = xs_dict_append(acct, "uri", snac.actor);
1466
1467    acct = xs_dict_append(acct, "locked",
1468        xs_stock(xs_is_true(xs_dict_get(snac.config, "approve_followers")) ? XSTYPE_TRUE : XSTYPE_FALSE));
1469
1470    acct = xs_dict_append(acct, "bot", xs_stock(xs_is_true(bot) ? XSTYPE_TRUE : XSTYPE_FALSE));
1471    acct = xs_dict_append(acct, "emojis", xs_stock(XSTYPE_LIST));
1472
1473    xs *src = xs_json_loads("{\"privacy\":\"public\", \"language\":\"en\","
1474        "\"follow_requests_count\": 0,"
1475        "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
1476    /* some apps take the note from the source object */
1477    src = xs_dict_set(src, "note", xs_dict_get(snac.config, "bio"));
1478    src = xs_dict_set(src, "privacy", xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE ? "private" : "public");
1479
1480    const xs_str *cw = xs_dict_get(snac.config, "cw");
1481    src = xs_dict_set(src, "sensitive",
1482        strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1483
1484    src = xs_dict_set(src, "bot", xs_stock(xs_is_true(bot) ? XSTYPE_TRUE : XSTYPE_FALSE));
1485
1486    xs *avatar = NULL;
1487    const char *av = xs_dict_get(snac.config, "avatar");
1488
1489    if (xs_is_null(av) || *av == '\0')
1490        avatar = xs_fmt("%s/susie.png", srv_baseurl);
1491    else
1492        avatar = xs_dup(av);
1493
1494    acct = xs_dict_append(acct, "avatar", avatar);
1495    acct = xs_dict_append(acct, "avatar_static", avatar);
1496
1497    xs *header = NULL;
1498    const char *hd = xs_dict_get(snac.config, "header");
1499
1500    if (!xs_is_null(hd))
1501        header = xs_dup(hd);
1502    else
1503        header = xs_str_new(NULL);
1504
1505    acct = xs_dict_append(acct, "header", header);
1506    acct = xs_dict_append(acct, "header_static", header);
1507
1508    xs *metadata = NULL;
1509    const xs_dict *md = xs_dict_get(snac.config, "metadata");
1510
1511    if (xs_is_dict(md))
1512        metadata = xs_dup(md);
1513    else
1514    if (xs_is_string(md)) {
1515        metadata = xs_dict_new();
1516        xs *l = xs_split(md, "\n");
1517        const char *ll;
1518
1519        xs_list_foreach(l, ll) {
1520            xs *kv = xs_split_n(ll, "=", 1);
1521            const char *k = xs_list_get(kv, 0);
1522            const char *v = xs_list_get(kv, 1);
1523
1524            if (k && v) {
1525                xs *kk = xs_strip_i(xs_dup(k));
1526                xs *vv = xs_strip_i(xs_dup(v));
1527                metadata = xs_dict_set(metadata, kk, vv);
1528            }
1529        }
1530    }
1531
1532    if (xs_is_dict(metadata)) {
1533        xs *fields = xs_list_new();
1534        const xs_str *k;
1535        const xs_str *v;
1536
1537        xs_dict *val_links = snac.links;
1538        if (xs_is_null(val_links))
1539            val_links = xs_stock(XSTYPE_DICT);
1540
1541        int c = 0;
1542        while (xs_dict_next(metadata, &k, &v, &c)) {
1543            xs *val_date = NULL;
1544
1545            const xs_number *verified_time = xs_dict_get(val_links, v);
1546            if (xs_type(verified_time) == XSTYPE_NUMBER) {
1547                time_t t = xs_number_get(verified_time);
1548
1549                if (t > 0)
1550                    val_date = xs_str_utctime(t, ISO_DATE_SPEC);
1551            }
1552
1553            xs *d = xs_dict_new();
1554
1555            d = xs_dict_append(d, "name", k);
1556            d = xs_dict_append(d, "value", v);
1557            d = xs_dict_append(d, "verified_at",
1558                               xs_type(val_date) == XSTYPE_STRING && *val_date ? val_date : xs_stock(XSTYPE_NULL));
1559
1560            fields = xs_list_append(fields, d);
1561        }
1562
1563        acct = xs_dict_set(acct, "fields", fields);
1564        /* some apps take the fields from the source object */
1565        src = xs_dict_set(src, "fields", fields);
1566    }
1567
1568    acct = xs_dict_append(acct, "source", src);
1569    acct = xs_dict_append(acct, "followers_count", xs_stock(0));
1570    acct = xs_dict_append(acct, "following_count", xs_stock(0));
1571    acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
1572
1573    /* does this user want to publish their contact metrics? */
1574    if (xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"))) {
1575        int fwing = following_list_len(&snac);
1576        int fwers = follower_list_len(&snac);
1577        xs *ni = xs_number_new(fwing);
1578        xs *ne = xs_number_new(fwers);
1579
1580        acct = xs_dict_append(acct, "followers_count", ne);
1581        acct = xs_dict_append(acct, "following_count", ni);
1582    }
1583
1584    *body = xs_json_dumps(acct, 4);
1585    *ctype = "application/json";
1586    *status = HTTP_STATUS_OK;
1587}
1588
1589
1590xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn)
1591{
1592    xs_list *out = xs_list_new();
1593    FILE *f;
1594    char md5[MD5_HEX_SIZE];
1595
1596    if (dbglevel) {
1597        xs *js = xs_json_dumps(args, 0);
1598        srv_debug(1, xs_fmt("mastoapi_timeline args %s", js));
1599    }
1600
1601    if ((f = fopen(index_fn, "r")) == NULL)
1602        return out;
1603
1604    const char *o_max_id   = xs_dict_get(args, "max_id");
1605    const char *o_since_id = xs_dict_get(args, "since_id");
1606    const char *o_min_id   = xs_dict_get(args, "min_id"); /* unsupported old-to-new navigation */
1607    const char *limit_s  = xs_dict_get(args, "limit");
1608    int (*iterator)(FILE *, char *);
1609    int initial_status = 0;
1610    int ascending = 0;
1611    int limit = 0;
1612    int cnt   = 0;
1613
1614    xs *max_id   = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL;
1615    xs *since_id = o_since_id ? xs_tolower_i(xs_dup(o_since_id)) : NULL;
1616    xs *min_id   = o_min_id ? xs_tolower_i(xs_dup(o_min_id)) : NULL;
1617
1618    if (!xs_is_null(limit_s))
1619        limit = atoi(limit_s);
1620
1621    if (limit == 0)
1622        limit = 20;
1623
1624    if (min_id) {
1625        iterator = &index_asc_next;
1626        initial_status = index_asc_first(f, md5, MID_TO_MD5(min_id));
1627        ascending = 1;
1628    }
1629    else {
1630        iterator = &index_desc_next;
1631        initial_status = index_desc_first(f, md5, 0);
1632    }
1633
1634    xs_set entries;
1635    xs_set_init(&entries);
1636
1637    if (initial_status) {
1638        do {
1639            xs *msg = NULL;
1640
1641            /* only return entries older that max_id */
1642            if (max_id) {
1643                if (strcmp(md5, MID_TO_MD5(max_id)) == 0) {
1644                    max_id = xs_free(max_id);
1645                    if (ascending)
1646                        break;
1647                }
1648                if (!ascending)
1649                    continue;
1650            }
1651
1652            /* only returns entries newer than since_id */
1653            if (since_id) {
1654                if (strcmp(md5, MID_TO_MD5(since_id)) == 0) {
1655                    if (!ascending)
1656                        break;
1657                    since_id = xs_free(since_id);
1658                }
1659                if (ascending)
1660                    continue;
1661            }
1662
1663            /* get the entry */
1664            if (user) {
1665                if (!valid_status(timeline_get_by_md5(user, md5, &msg)))
1666                    continue;
1667            }
1668            else {
1669                if (!valid_status(object_get_by_md5(md5, &msg)))
1670                    continue;
1671            }
1672
1673            /* discard non-Notes */
1674            const char *id   = xs_dict_get(msg, "id");
1675            const char *type = xs_dict_get(msg, "type");
1676            if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
1677                continue;
1678
1679            if (id && is_instance_blocked(id))
1680                continue;
1681
1682            const char *from = NULL;
1683            if (strcmp(type, "Page") == 0)
1684                from = xs_dict_get(msg, "audience");
1685
1686            if (from == NULL)
1687                from = get_atto(msg);
1688
1689            if (from == NULL)
1690                continue;
1691
1692            if (user) {
1693                /* is this message from a person we don't follow? */
1694                if (strcmp(from, user->actor) && !following_check(user, from)) {
1695                    /* discard if it was not boosted */
1696                    xs *idx = object_announces(id);
1697
1698                    if (xs_list_len(idx) == 0)
1699                        continue;
1700                }
1701
1702                /* discard notes from muted morons */
1703                if (is_muted(user, from))
1704                    continue;
1705
1706                /* discard hidden notes */
1707                if (is_hidden(user, id))
1708                    continue;
1709            }
1710            else {
1711                /* skip non-public messages */
1712                if (!is_msg_public(msg))
1713                    continue;
1714
1715                /* discard messages from private users */
1716                if (is_msg_from_private_user(msg))
1717                    continue;
1718            }
1719
1720            /* if it has a name and it's not an object that may have one,
1721               it's a poll vote, so discard it */
1722            if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video|Audio|Event"))
1723                continue;
1724
1725            /* convert the Note into a Mastodon status */
1726            xs *st = mastoapi_status(user, msg);
1727
1728            if (st != NULL && xs_set_add(&entries, md5) == 1) {
1729                if (ascending)
1730                    out = xs_list_insert(out, 0, st);
1731                else
1732                    out = xs_list_append(out, st);
1733                cnt++;
1734            }
1735
1736        } while ((cnt < limit) && (*iterator)(f, md5));
1737    }
1738
1739    xs_set_free(&entries);
1740
1741    int more = index_desc_next(f, md5);
1742
1743    fclose(f);
1744
1745    srv_debug(1, xs_fmt("mastoapi_timeline ret %d%s", cnt, more ? " (+)" : ""));
1746
1747    return out;
1748}
1749
1750
1751xs_str *timeline_link_header(const char *endpoint, xs_list *timeline)
1752/* returns a Link header with paging information */
1753{
1754    xs_str *s = NULL;
1755
1756    if (xs_list_len(timeline) == 0)
1757        return NULL;
1758
1759    const xs_dict *first_st = xs_list_get(timeline, 0);
1760    const xs_dict *last_st  = xs_list_get(timeline, -1);
1761    const char *first_id    = xs_dict_get(first_st, "id");
1762    const char *last_id     = xs_dict_get(last_st, "id");
1763    const char *host        = xs_dict_get(srv_config, "host");
1764    const char *protocol    = xs_dict_get_def(srv_config, "protocol", "https");
1765
1766    s = xs_fmt(
1767        "<%s:/" "/%s%s?max_id=%s>; rel=\"next\", "
1768        "<%s:/" "/%s%s?since_id=%s>; rel=\"prev\"",
1769        protocol, host, endpoint, last_id,
1770        protocol, host, endpoint, first_id);
1771
1772    srv_debug(1, xs_fmt("timeline_link_header %s", s));
1773
1774    return s;
1775}
1776
1777
1778xs_list *mastoapi_account_lists(snac *user, const char *uid)
1779/* returns the list of list an user is in */
1780{
1781    xs_list *out  = xs_list_new();
1782    xs *actor_md5 = NULL;
1783    xs *lol       = list_maint(user, NULL, 0);
1784
1785    if (uid) {
1786        if (!xs_is_hex(uid))
1787            actor_md5 = xs_md5_hex(uid, strlen(uid));
1788        else
1789            actor_md5 = xs_dup(uid);
1790    }
1791
1792    const xs_list *li;
1793    xs_list_foreach(lol, li) {
1794        const char *list_id    = xs_list_get(li, 0);
1795        const char *list_title = xs_list_get(li, 1);
1796        if (uid) {
1797            xs *users = list_members(user, list_id, NULL, 0);
1798            if (xs_list_in(users, actor_md5) == -1)
1799                continue;
1800        }
1801
1802        xs *d = xs_dict_new();
1803
1804        d = xs_dict_append(d, "id", list_id);
1805        d = xs_dict_append(d, "title", list_title);
1806        d = xs_dict_append(d, "replies_policy", "list");
1807        d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
1808
1809        out = xs_list_append(out, d);
1810    }
1811
1812    return out;
1813}
1814
1815
1816xs_list *build_childrens(const xs_dict *msg, snac *snac1) {
1817    xs_list *ret = xs_list_new();
1818    xs *children = object_children(xs_dict_get(msg, "id"));
1819    char *p = children;
1820    const xs_str *v;
1821
1822    while (xs_list_iter(&p, &v)) {
1823        xs *m2 = NULL;
1824
1825        if (valid_status(timeline_get_by_md5(snac1, v, &m2))) {
1826            if (xs_is_null(xs_dict_get(m2, "name"))) {
1827                xs *st = mastoapi_status(snac1, m2);
1828
1829                if (st) {
1830                    /* childrens children */
1831                    xs *childs = build_childrens(m2, snac1);
1832                    ret = xs_list_append(ret, st);
1833                    if (xs_list_len(childs)) {
1834                        char *p2 = childs;
1835                        while (xs_list_iter(&p2, &v))
1836                            ret = xs_list_append(ret, v);
1837
1838                    }
1839                }
1840            }
1841        }
1842    }
1843    return ret;
1844}
1845
1846
1847int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1848                         char **body, int *b_size, char **ctype, xs_str **link)
1849{
1850    (void)b_size;
1851
1852    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
1853        return 0;
1854
1855    int status    = HTTP_STATUS_NOT_FOUND;
1856    const xs_dict *args = xs_dict_get(req, "q_vars");
1857    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
1858
1859    snac snac1 = {0};
1860    int logged_in = process_auth_token(&snac1, req);
1861
1862    if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
1863        if (logged_in) {
1864            credentials_get(body, ctype, &status, snac1);
1865        }
1866        else {
1867            status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login)
1868        }
1869    }
1870    else
1871    if (strcmp(cmd, "/v1/accounts/relationships") == 0) { /** **/
1872        /* find if an account is followed, blocked, etc. */
1873        /* the account to get relationships about is in args "id[]" */
1874
1875        if (logged_in) {
1876            xs *res         = xs_list_new();
1877            const char *md5 = xs_dict_get(args, "id[]");
1878
1879            if (xs_is_null(md5))
1880                md5 = xs_dict_get(args, "id");
1881
1882            if (!xs_is_null(md5)) {
1883                if (xs_type(md5) == XSTYPE_LIST)
1884                    md5 = xs_list_get(md5, 0);
1885
1886                xs *rel = mastoapi_relationship(&snac1, md5);
1887
1888                if (rel != NULL)
1889                    res = xs_list_append(res, rel);
1890            }
1891
1892            *body  = xs_json_dumps(res, 4);
1893            *ctype = "application/json";
1894            status = HTTP_STATUS_OK;
1895        }
1896        else
1897            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
1898    }
1899    else
1900    if (strcmp(cmd, "/v1/accounts/lookup") == 0) { /** **/
1901        /* lookup an account */
1902        const char *acct = xs_dict_get(args, "acct");
1903
1904        if (!xs_is_null(acct)) {
1905            xs *s = xs_strip_chars_i(xs_dup(acct), "@");
1906            xs *l = xs_split_n(s, "@", 1);
1907            const char *uid = xs_list_get(l, 0);
1908            const char *host = xs_list_get(l, 1);
1909
1910            if (uid && (!host || strcmp(host, xs_dict_get(srv_config, "host")) == 0)) {
1911                snac user;
1912
1913                if (user_open(&user, uid)) {
1914                    xs *actor = msg_actor(&user);
1915                    xs *macct = mastoapi_account(NULL, actor);
1916
1917                    *body  = xs_json_dumps(macct, 4);
1918                    *ctype = "application/json";
1919                    status = HTTP_STATUS_OK;
1920
1921                    user_free(&user);
1922                }
1923            }
1924        }
1925    }
1926    else
1927    if (xs_startswith(cmd, "/v1/accounts/")) { /** **/
1928        /* account-related information */
1929        xs *l = xs_split(cmd, "/");
1930        const char *uid = xs_list_get(l, 3);
1931        const char *opt = xs_list_get(l, 4);
1932
1933        if (uid != NULL) {
1934            snac snac2;
1935            xs *out   = NULL;
1936            xs *actor = NULL;
1937
1938            if (logged_in && strcmp(uid, "familiar_followers") == 0) { /** **/
1939                /* familiar followers endpoint - return empty array */
1940                out = xs_list_new();
1941            }
1942            else
1943            if (logged_in && strcmp(uid, "search") == 0) { /** **/
1944                /* search for accounts starting with q */
1945                const char *aq = xs_dict_get(args, "q");
1946
1947                if (!xs_is_null(aq)) {
1948                    xs *q    = xs_utf8_to_lower(aq);
1949                    out      = xs_list_new();
1950                    xs *wing = following_list(&snac1);
1951                    xs *wers = follower_list(&snac1);
1952                    xs *ulst = user_list();
1953                    xs_list *p;
1954                    const xs_str *v;
1955                    xs_set seen;
1956
1957                    xs_set_init(&seen);
1958
1959                    /* user relations */
1960                    xs_list *lsts[] = { wing, wers, NULL };
1961                    int n;
1962                    for (n = 0; (p = lsts[n]) != NULL; n++) {
1963
1964                        while (xs_list_iter(&p, &v)) {
1965                            /* already seen? skip */
1966                            if (xs_set_add(&seen, v) == 0)
1967                                continue;
1968
1969                            xs *actor = NULL;
1970
1971                            if (valid_status(object_get(v, &actor))) {
1972                                const char *uname = xs_dict_get(actor, "preferredUsername");
1973
1974                                if (!xs_is_null(uname)) {
1975                                    xs *luname = xs_tolower_i(xs_dup(uname));
1976
1977                                    if (xs_startswith(luname, q)) {
1978                                        xs *acct = mastoapi_account(&snac1, actor);
1979
1980                                        out = xs_list_append(out, acct);
1981                                    }
1982                                }
1983                            }
1984                        }
1985                    }
1986
1987                    /* local users */
1988                    p = ulst;
1989                    while (xs_list_iter(&p, &v)) {
1990                        snac user;
1991
1992                        /* skip this same user */
1993                        if (strcmp(v, xs_dict_get(snac1.config, "uid")) == 0)
1994                            continue;
1995
1996                        /* skip if the uid does not start with the query */
1997                        xs *v2 = xs_tolower_i(xs_dup(v));
1998                        if (!xs_startswith(v2, q))
1999                            continue;
2000
2001                        if (user_open(&user, v)) {
2002                            /* if it's not already seen, add it */
2003                            if (xs_set_add(&seen, user.actor) == 1) {
2004                                xs *actor = msg_actor(&user);
2005                                xs *acct  = mastoapi_account(&snac1, actor);
2006
2007                                out = xs_list_append(out, acct);
2008                            }
2009
2010                            user_free(&user);
2011                        }
2012                    }
2013
2014                    xs_set_free(&seen);
2015                }
2016            }
2017            else
2018            /* is it a local user? */
2019            if (user_open(&snac2, uid) || user_open_by_md5(&snac2, uid)) {
2020                if (opt == NULL) {
2021                    /* account information */
2022                    actor = msg_actor(&snac2);
2023                    out   = mastoapi_account(NULL, actor);
2024                }
2025                else
2026                if (strcmp(opt, "statuses") == 0) { /** **/
2027                    if (logged_in || xs_type(xs_dict_get(snac2.config, "private")) == XSTYPE_FALSE) {
2028                        /* the public list of posts of a user */
2029                        const char *limit_s  = xs_dict_get(args, "limit");
2030                        const char *o_max_id = xs_dict_get(args, "max_id");
2031                        int limit = limit_s ? atoi(limit_s) : 20;
2032                        xs *max_id = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL;
2033
2034                        srv_debug(1, xs_fmt("account statuses: max_id=%s limit=%d", max_id ? max_id : "(null)", limit));
2035
2036                        xs *timeline = timeline_simple_list(&snac2, "public", 0, 256, NULL);
2037                        xs_list *p   = timeline;
2038                        const xs_str *v;
2039                        xs_set seen;
2040                        int cnt = 0;
2041                        int skip_until_max = max_id != NULL;
2042
2043                        out = xs_list_new();
2044                        xs_set_init(&seen);
2045
2046                        while (xs_list_iter(&p, &v) && cnt < limit) {
2047                            xs *msg = NULL;
2048
2049                            if (valid_status(timeline_get_by_md5(&snac2, v, &msg))) {
2050                                const char *msg_id = xs_dict_get(msg, "id");
2051
2052                                /* add only posts by the author */
2053                                if (!xs_is_null(msg_id) &&
2054                                    strcmp(xs_dict_get(msg, "type"), "Note") == 0 &&
2055                                    is_msg_mine(&snac2, xs_dict_get(msg, "id")) && is_msg_public(msg)) {
2056
2057                                    /* if max_id is set, skip entries until we find it */
2058                                    if (skip_until_max) {
2059                                        xs *mid = mastoapi_id(msg);
2060                                        if (strcmp(mid, max_id) == 0) {
2061                                            skip_until_max = 0;
2062                                            srv_debug(2, xs_fmt("account statuses: found max_id, starting from next post"));
2063                                        }
2064                                        continue;
2065                                    }
2066
2067                                    /* deduplicate by message id */
2068                                    if (xs_set_add(&seen, msg_id) == 1) {
2069                                        xs *st = mastoapi_status(&snac2, msg);
2070
2071                                        if (st) {
2072                                            out = xs_list_append(out, st);
2073                                            cnt++;
2074                                        }
2075                                    }
2076                                }
2077                            }
2078                        }
2079
2080                        srv_debug(1, xs_fmt("account statuses: returning %d posts (requested %d)", cnt, limit));
2081                        xs_set_free(&seen);
2082                    }
2083                    else
2084                        status = HTTP_STATUS_UNAUTHORIZED;
2085                }
2086                else
2087                if (strcmp(opt, "featured_tags") == 0) {
2088                    /* snac doesn't have features tags, yet? */
2089                    /* implement empty response so apps like Tokodon don't show an error */
2090                    out = xs_list_new();
2091                }
2092                else
2093                if (strcmp(opt, "following") == 0) {
2094                    xs *wing = following_list(&snac1);
2095                    out = xs_list_new();
2096                    int c = 0;
2097                    const char *v;
2098
2099                    while (xs_list_next(wing, &v, &c)) {
2100                        xs *actor = NULL;
2101
2102                        if (valid_status(object_get(v, &actor))) {
2103                            xs *acct = mastoapi_account(NULL, actor);
2104                            out = xs_list_append(out, acct);
2105                        }
2106                    }
2107                }
2108                else
2109                if (strcmp(opt, "followers") == 0) {
2110                    out = xs_list_new();
2111                }
2112                else
2113                if (strcmp(opt, "lists") == 0) {
2114                    out = mastoapi_account_lists(&snac1, uid);
2115                }
2116                else
2117                if (strcmp(opt, "familiar_followers") == 0) {
2118                    /* familiar followers - not implemented, return empty array */
2119                    out = xs_list_new();
2120                }
2121
2122                user_free(&snac2);
2123            }
2124            else {
2125                /* try the uid as the md5 of a possibly loaded actor */
2126                if (logged_in && valid_status(object_get_by_md5(uid, &actor))) {
2127                    if (opt == NULL) {
2128                        /* account information */
2129                        out = mastoapi_account(&snac1, actor);
2130                    }
2131                    else
2132                    if (strcmp(opt, "statuses") == 0) {
2133                        /* fetch statuses from remote outbox */
2134                        out = xs_list_new();
2135                        const char *outbox_url = xs_dict_get(actor, "outbox");
2136
2137                        if (!xs_is_null(outbox_url)) {
2138                            /* extract query parameters */
2139                            const char *limit_s = xs_dict_get(args, "limit");
2140                            const char *exclude_replies_s = xs_dict_get(args, "exclude_replies");
2141                            const char *o_max_id = xs_dict_get(args, "max_id");
2142
2143                            int limit = 20;
2144                            if (!xs_is_null(limit_s))
2145                                limit = atoi(limit_s);
2146                            if (limit == 0 || limit > 40)
2147                                limit = 20;
2148
2149                            int exclude_replies = !xs_is_null(exclude_replies_s) &&
2150                                                 strcmp(exclude_replies_s, "true") == 0;
2151
2152                            xs *max_id = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL;
2153                            int skip_until_max = max_id != NULL;
2154
2155                            srv_debug(1, xs_fmt("remote account statuses: fetching from %s (max_id=%s limit=%d)",
2156                                               outbox_url, max_id ? max_id : "(null)", limit));
2157
2158                            /* fetch first page only - safer for memory on large instances */
2159                            xs *outbox_collection = NULL;
2160                            xs *first_page = NULL;
2161                            const xs_list *items = get_collection_items(&snac1, outbox_url,
2162                                                                        &outbox_collection, &first_page);
2163
2164                            int count = 0;
2165                            int processed = 0;
2166
2167                            if (!xs_is_null(items) && xs_type(items) == XSTYPE_LIST) {
2168                                int total_items = xs_list_len(items);
2169                                srv_debug(1, xs_fmt("remote account statuses: got %d items from outbox", total_items));
2170
2171                                const xs_val *item;
2172
2173                                xs_list_foreach(items, item) {
2174                                    processed++;
2175
2176                                    if (count >= limit)
2177                                        break;
2178
2179                                    const xs_dict *post = extract_post_from_item(item);
2180                                    if (!post)
2181                                        continue;
2182
2183                                    const char *post_type = xs_dict_get(post, "type");
2184                                    const char *in_reply_to = xs_dict_get(post, "inReplyTo");
2185
2186                                    /* apply filters */
2187                                    if (exclude_replies && !xs_is_null(in_reply_to))
2188                                        continue;
2189
2190                                    if (is_valid_post_type(post_type)) {
2191                                        /* store object locally so mastoapi_id() can generate valid IDs */
2192                                        const char *post_id = xs_dict_get(post, "id");
2193                                        if (!xs_is_null(post_id))
2194                                            object_add(post_id, post);
2195
2196                                        /* handle pagination with max_id */
2197                                        if (skip_until_max) {
2198                                            xs *mid = mastoapi_id(post);
2199                                            if (!xs_is_null(mid) && strcmp(mid, max_id) == 0) {
2200                                                skip_until_max = 0;
2201                                                srv_debug(2, xs_fmt("remote account statuses: found max_id at position %d", processed));
2202                                            }
2203                                            continue;
2204                                        }
2205
2206                                        /* pass logged-in user context to enable media proxying if configured */
2207                                        xs *st = mastoapi_status(&snac1, post);
2208                                        if (st) {
2209                                            out = xs_list_append(out, st);
2210                                            count++;
2211                                        }
2212                                    }
2213                                }
2214                            }
2215                            else {
2216                                srv_debug(1, xs_fmt("remote account statuses: no items found in outbox"));
2217                            }
2218
2219                            srv_debug(1, xs_fmt("remote account statuses: processed %d items, returning %d posts (requested %d)",
2220                                               processed, count, limit));
2221                        }
2222                    }
2223                    else
2224                    if (strcmp(opt, "featured_tags") == 0) {
2225                        /* snac doesn't have features tags, yet? */
2226                        /* implement empty response so apps like Tokodon don't show an error */
2227                        out = xs_list_new();
2228                    }
2229                    else
2230                    if (strcmp(opt, "lists") == 0) {
2231                        out = mastoapi_account_lists(&snac1, uid);
2232                    }
2233                    else
2234                    if (strcmp(opt, "familiar_followers") == 0) {
2235                        /* familiar followers - not implemented, return empty array */
2236                        out = xs_list_new();
2237                    }
2238                }
2239            }
2240
2241            if (out != NULL) {
2242                *body  = xs_json_dumps(out, 4);
2243                *ctype = "application/json";
2244                status = HTTP_STATUS_OK;
2245            }
2246        }
2247    }
2248    else
2249    if (strcmp(cmd, "/v1/timelines/home") == 0) { /** **/
2250        /* the private timeline */
2251        if (logged_in) {
2252            xs *ifn = user_index_fn(&snac1, "private");
2253            xs *out = mastoapi_timeline(&snac1, args, ifn);
2254
2255            *link = timeline_link_header("/api/v1/timelines/home", out);
2256
2257            *body  = xs_json_dumps(out, 4);
2258            *ctype = "application/json";
2259            status = HTTP_STATUS_OK;
2260
2261            srv_debug(2, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out)));
2262        }
2263        else {
2264            status = HTTP_STATUS_UNAUTHORIZED;
2265        }
2266    }
2267    else
2268    if (strcmp(cmd, "/v1/timelines/public") == 0) { /** **/
2269        /* the instance public timeline (public timelines for all users) */
2270        xs *ifn = instance_index_fn();
2271        xs *out = mastoapi_timeline(NULL, args, ifn);
2272
2273        *body  = xs_json_dumps(out, 4);
2274        *ctype = "application/json";
2275        status = HTTP_STATUS_OK;
2276    }
2277    else
2278    if (xs_startswith(cmd, "/v1/timelines/tag/")) { /** **/
2279        /* get the tag */
2280        xs *l = xs_split(cmd, "/");
2281        const char *tag = xs_list_get(l, -1);
2282
2283        xs *ifn = tag_fn(tag);
2284        xs *out = mastoapi_timeline(NULL, args, ifn);
2285
2286        *body  = xs_json_dumps(out, 4);
2287        *ctype = "application/json";
2288        status = HTTP_STATUS_OK;
2289    }
2290    else
2291    if (xs_startswith(cmd, "/v1/timelines/list/")) { /** **/
2292        /* get the list id */
2293        if (logged_in) {
2294            xs *l = xs_split(cmd, "/");
2295            const char *list = xs_list_get(l, -1);
2296
2297            xs *ifn = list_timeline_fn(&snac1, list);
2298            xs *out = mastoapi_timeline(NULL, args, ifn);
2299
2300            *body  = xs_json_dumps(out, 4);
2301            *ctype = "application/json";
2302            status = HTTP_STATUS_OK;
2303        }
2304        else
2305            status = HTTP_STATUS_MISDIRECTED_REQUEST;
2306    }
2307    else
2308    if (strcmp(cmd, "/v1/conversations") == 0) { /** **/
2309        /* TBD */
2310        *body  = xs_dup("[]");
2311        *ctype = "application/json";
2312        status = HTTP_STATUS_OK;
2313    }
2314    else
2315    if (strcmp(cmd, "/v1/notifications") == 0) { /** **/
2316        if (logged_in) {
2317            xs *l      = notify_list(&snac1, 0, 64);
2318            xs *out    = xs_list_new();
2319            const char *v;
2320            const xs_list *excl = xs_dict_get(args, "exclude_types[]");
2321            const xs_list *incl = xs_dict_get(args, "types[]");
2322            const char *min_id = xs_dict_get(args, "min_id");
2323            const char *max_id = xs_dict_get(args, "max_id");
2324            const char *limit = xs_dict_get(args, "limit");
2325            int limit_count = 0;
2326            if (xs_is_string(limit)) {
2327                limit_count = atoi(limit);
2328            }
2329
2330            if (dbglevel) {
2331                xs *js = xs_json_dumps(args, 0);
2332                srv_debug(1, xs_fmt("mastoapi_notifications args %s", js));
2333            }
2334
2335            xs_list_foreach(l, v) {
2336                xs *noti = notify_get(&snac1, v);
2337
2338                if (noti == NULL)
2339                    continue;
2340
2341                const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0);
2342
2343                const char *type  = xs_dict_get(noti, "type");
2344                const char *utype = xs_dict_get(noti, "utype");
2345                const char *objid = xs_dict_get(noti, "objid");
2346                const char *id    = xs_dict_get(noti, "id");
2347                const char *actid = xs_dict_get(noti, "actor");
2348
2349                int isEmoji = 0;
2350
2351                xs *fid = xs_replace(id, ".", "");
2352                xs *actor = NULL;
2353                xs *entry = NULL;
2354
2355                if (tag) {
2356                    isEmoji = strcmp(xs_dict_get(tag, "type"), "Emoji") ? 0 : 1;
2357                }
2358
2359                if (!valid_status(actor_get(actid, &actor)))
2360                    continue;
2361
2362                if (objid != NULL && !valid_status(object_get(objid, &entry)))
2363                    continue;
2364
2365                if (is_hidden(&snac1, objid))
2366                    continue;
2367
2368                if (max_id) {
2369                    if (strcmp(fid, max_id) == 0)
2370                        max_id = NULL;
2371
2372                    continue;
2373                }
2374
2375                if (min_id) {
2376                    if (strcmp(fid, min_id) <= 0) {
2377                        continue;
2378                    }
2379                }
2380
2381                /* convert the type */
2382                if (strcmp(type, "Like") == 0 && !isEmoji)
2383                    type = "favourite";
2384                else
2385                if (isEmoji || strcmp(type, "EmojiReact") == 0)
2386                    type = "reaction";
2387                else
2388                if (strcmp(type, "Announce") == 0)
2389                    type = "reblog";
2390                else
2391                if (strcmp(type, "Follow") == 0)
2392                    type = "follow";
2393                else
2394                if (strcmp(type, "Create") == 0)
2395                    type = "mention";
2396                else
2397                if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
2398                    type = "poll";
2399                else
2400                    continue;
2401
2402                /* excluded type? */
2403                if (xs_is_list(excl) && xs_list_in(excl, type) != -1)
2404                    continue;
2405
2406                /* included type? */
2407                if (xs_is_list(incl) && xs_list_in(incl, type) == -1)
2408                    continue;
2409
2410                xs *mn = xs_dict_new();
2411
2412                mn = xs_dict_append(mn, "type", type);
2413
2414                mn = xs_dict_append(mn, "id", fid);
2415
2416                mn = xs_dict_append(mn, "created_at", xs_dict_get(noti, "date"));
2417
2418                xs *acct = mastoapi_account(&snac1, actor);
2419
2420                if (acct == NULL)
2421                    continue;
2422
2423                mn = xs_dict_append(mn, "account", acct);
2424
2425                if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) {
2426                    xs *st = mastoapi_status(&snac1, entry);
2427
2428                    if (st) {
2429                        mn = xs_dict_append(mn, "status", st);
2430
2431                        if (strcmp(type, "reaction") == 0 && !xs_is_null(objid)) {
2432                            const char *eid = NULL;
2433                            const char *url = NULL;
2434                            int utf = 0;
2435
2436                            const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0);
2437                            const char *content = xs_dict_get_path(noti, "msg.content");
2438
2439                            url = xs_dict_get(xs_dict_get(tag, "icon"), "url");
2440                            eid = xs_dict_get(tag, "name");
2441
2442                            if (eid && url) {
2443                                mn = xs_dict_append(mn, "emoji", eid);
2444                                mn = xs_dict_append(mn, "emoji_url", url);
2445                            }
2446
2447                            if (xs_is_emoji((utf = xs_utf8_dec(&content)))) {
2448                                xs *s1 = xs_fmt("&#%d;", utf);
2449                                mn = xs_dict_append(mn, "name", s1);
2450                            }
2451                        }
2452                    }
2453                }
2454
2455                out = xs_list_append(out, mn);
2456
2457                if (--limit_count <= 0)
2458                    break;
2459            }
2460
2461            srv_debug(1, xs_fmt("mastoapi_notifications count %d", xs_list_len(out)));
2462
2463            *body  = xs_json_dumps(out, 4);
2464            *ctype = "application/json";
2465            status = HTTP_STATUS_OK;
2466        }
2467        else
2468            status = HTTP_STATUS_UNAUTHORIZED;
2469    }
2470    else
2471    if (strcmp(cmd, "/v1/filters") == 0) { /** **/
2472        /* snac will never have filters */
2473        *body  = xs_dup("[]");
2474        *ctype = "application/json";
2475        status = HTTP_STATUS_OK;
2476    }
2477    else
2478    if (strcmp(cmd, "/v2/filters") == 0) { /** **/
2479        /* snac will never have filters
2480         * but still, without a v2 endpoint a short delay is introduced
2481         * in some apps */
2482        *body  = xs_dup("[]");
2483        *ctype = "application/json";
2484        status = HTTP_STATUS_OK;
2485    }
2486    else
2487    if (strcmp(cmd, "/v1/favourites") == 0) { /** **/
2488        /* snac will never support a list of favourites */
2489        *body  = xs_dup("[]");
2490        *ctype = "application/json";
2491        status = HTTP_STATUS_OK;
2492    }
2493    else
2494    if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/
2495        if (logged_in) {
2496            xs *ifn = bookmark_index_fn(&snac1);
2497            xs *out = mastoapi_timeline(&snac1, args, ifn);
2498
2499            *body  = xs_json_dumps(out, 4);
2500            *ctype = "application/json";
2501            status = HTTP_STATUS_OK;
2502        }
2503        else
2504            status = HTTP_STATUS_UNAUTHORIZED;
2505    }
2506    else
2507    if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
2508        if (logged_in) {
2509            xs *l  = mastoapi_account_lists(&snac1, NULL);
2510
2511            *body  = xs_json_dumps(l, 4);
2512            *ctype = "application/json";
2513            status = HTTP_STATUS_OK;
2514        }
2515        else
2516            status = HTTP_STATUS_UNAUTHORIZED;
2517    }
2518    else
2519    if (xs_startswith(cmd, "/v1/lists/")) { /** list information **/
2520        if (logged_in) {
2521            xs *l = xs_split(cmd, "/");
2522            const char *p = xs_list_get(l, -1);
2523
2524            if (p) {
2525                if (strcmp(p, "accounts") == 0) {
2526                    p = xs_list_get(l, -2);
2527
2528                    if (p && xs_is_hex(p)) {
2529                        xs *actors = list_members(&snac1, p, NULL, 0);
2530                        xs *out = xs_list_new();
2531                        int c = 0;
2532                        const char *v;
2533
2534                        while (xs_list_next(actors, &v, &c)) {
2535                            xs *actor = NULL;
2536
2537                            if (valid_status(object_get_by_md5(v, &actor))) {
2538                                xs *acct = mastoapi_account(&snac1, actor);
2539                                out = xs_list_append(out, acct);
2540                            }
2541                        }
2542
2543                        *body  = xs_json_dumps(out, 4);
2544                        *ctype = "application/json";
2545                        status = HTTP_STATUS_OK;
2546                    }
2547                }
2548                else
2549                if (xs_is_hex(p)) {
2550                    xs *out = xs_list_new();
2551                    xs *lol = list_maint(&snac1, NULL, 0);
2552                    int c = 0;
2553                    const xs_list *v;
2554
2555                    while (xs_list_next(lol, &v, &c)) {
2556                        const char *id = xs_list_get(v, 0);
2557
2558                        if (id && strcmp(id, p) == 0) {
2559                            xs *d = xs_dict_new();
2560
2561                            d = xs_dict_append(d, "id", p);
2562                            d = xs_dict_append(d, "title", xs_list_get(v, 1));
2563                            d = xs_dict_append(d, "replies_policy", "list");
2564                            d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
2565
2566                            out = xs_dup(d);
2567                            break;
2568                        }
2569                    }
2570
2571                    *body  = xs_json_dumps(out, 4);
2572                    *ctype = "application/json";
2573                    status = HTTP_STATUS_OK;
2574                }
2575            }
2576        }
2577        else
2578            status = HTTP_STATUS_UNAUTHORIZED;
2579    }
2580    else
2581    if (strcmp(cmd, "/v1/scheduled_statuses") == 0) { /** **/
2582        /* TBD */
2583        *body  = xs_dup("[]");
2584        *ctype = "application/json";
2585        status = HTTP_STATUS_OK;
2586    }
2587    else
2588    if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/
2589        if (logged_in) {
2590            xs *pend = pending_list(&snac1);
2591            xs *resp = xs_list_new();
2592            const char *id;
2593
2594            xs_list_foreach(pend, id) {
2595                xs *actor = NULL;
2596
2597                if (valid_status(object_get(id, &actor))) {
2598                    xs *acct = mastoapi_account(&snac1, actor);
2599
2600                    if (acct)
2601                        resp = xs_list_append(resp, acct);
2602                }
2603            }
2604
2605            *body  = xs_json_dumps(resp, 4);
2606            *ctype = "application/json";
2607            status = HTTP_STATUS_OK;
2608        }
2609    }
2610    else
2611    if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
2612        if (logged_in) {
2613            xs *resp = xs_list_new();
2614            double la = 0.0;
2615            xs *user_la = xs_dup(xs_dict_get(snac1.config, "last_announcement"));
2616            if (user_la != NULL)
2617                la = xs_number_get(user_la);
2618            xs *val_date = xs_str_utctime(la, ISO_DATE_SPEC);
2619
2620            /* contrary to html, we always send the announcement and set the read flag instead */
2621
2622            const t_announcement *annce = announcement(la);
2623            if (annce != NULL && annce->text != NULL) {
2624                xs *an = xs_dict_new();
2625                xs *id = xs_fmt("%d", annce->timestamp);
2626                xs *ct = xs_fmt("<p>%s</p>", annce->text);
2627
2628                an = xs_dict_set(an, "id",           id);
2629                an = xs_dict_set(an, "content",      ct);
2630                an = xs_dict_set(an, "starts_at",    xs_stock(XSTYPE_NULL));
2631                an = xs_dict_set(an, "ends_at",      xs_stock(XSTYPE_NULL));
2632                an = xs_dict_set(an, "all_day",      xs_stock(XSTYPE_TRUE));
2633                an = xs_dict_set(an, "published_at", val_date);
2634                an = xs_dict_set(an, "updated_at",   val_date);
2635                an = xs_dict_set(an, "read",         (annce->timestamp >= la)
2636                    ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE));
2637                an = xs_dict_set(an, "mentions",     xs_stock(XSTYPE_LIST));
2638                an = xs_dict_set(an, "statuses",     xs_stock(XSTYPE_LIST));
2639                an = xs_dict_set(an, "tags",         xs_stock(XSTYPE_LIST));
2640                an = xs_dict_set(an, "emojis",       xs_stock(XSTYPE_LIST));
2641                an = xs_dict_set(an, "reactions",    xs_stock(XSTYPE_LIST));
2642                resp = xs_list_append(resp, an);
2643            }
2644
2645            *body  = xs_json_dumps(resp, 4);
2646            *ctype = "application/json";
2647            status = HTTP_STATUS_OK;
2648        }
2649    }
2650    else
2651    if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
2652        xs *emo = emojis();
2653        xs *list = xs_list_new();
2654        const xs_str *k;
2655        const xs_val *v;
2656        xs_dict_foreach(emo, k, v) {
2657            xs *current = xs_dict_new();
2658            if ((xs_startswith(v, "https://") && xs_startswith((xs_mime_by_ext(v)), "image/")) || xs_type(v) == XSTYPE_DICT) {
2659                /* remove first and last colon */
2660                if (xs_type(v) == XSTYPE_DICT) {
2661                    const char *v2;
2662                    const char *cat = k;
2663                    xs_dict_foreach(v, k, v2) {
2664                        xs *shortcode = xs_replace(k, ":", "");
2665                        current = xs_dict_append(current, "shortcode", shortcode);
2666                        current = xs_dict_append(current, "url", v2);
2667                        current = xs_dict_append(current, "static_url", v2);
2668                        current = xs_dict_append(current, "visible_in_picker", xs_stock(XSTYPE_TRUE));
2669                        current = xs_dict_append(current, "category", cat);
2670                        list = xs_list_append(list, current);
2671                    }
2672                }
2673                else {
2674                    xs *shortcode = xs_replace(k, ":", "");
2675                    current = xs_dict_append(current, "shortcode", shortcode);
2676                    current = xs_dict_append(current, "url", v);
2677                    current = xs_dict_append(current, "static_url", v);
2678                    current = xs_dict_append(current, "visible_in_picker", xs_stock(XSTYPE_TRUE));
2679                    list = xs_list_append(list, current);
2680                }
2681            }
2682        }
2683        *body  = xs_json_dumps(list, 0);
2684        *ctype = "application/json";
2685        status = HTTP_STATUS_OK;
2686    }
2687    else
2688    if (strcmp(cmd, "/v1/instance") == 0) { /** **/
2689        /* returns an instance object */
2690        xs *ins = xs_dict_new();
2691        const char *host  = xs_dict_get(srv_config, "host");
2692        const char *title = xs_dict_get(srv_config, "title");
2693        const char *sdesc = xs_dict_get(srv_config, "short_description");
2694
2695        ins = xs_dict_append(ins, "uri",         host);
2696        ins = xs_dict_append(ins, "domain",      host);
2697        ins = xs_dict_append(ins, "title",       title && *title ? title : host);
2698        ins = xs_dict_append(ins, "version",     "4.0.0 (not true; really " USER_AGENT ")");
2699        ins = xs_dict_append(ins, "source_url",  WHAT_IS_SNAC_URL);
2700        ins = xs_dict_append(ins, "description", host);
2701
2702        ins = xs_dict_append(ins, "short_description", sdesc && *sdesc ? sdesc : host);
2703
2704        xs *susie = xs_fmt("%s/susie.png", srv_baseurl);
2705        ins = xs_dict_append(ins, "thumbnail", susie);
2706
2707        const char *v = xs_dict_get(srv_config, "admin_email");
2708        if (xs_is_null(v) || *v == '\0')
2709            v = "admin@localhost";
2710
2711        ins = xs_dict_append(ins, "email", v);
2712
2713        ins = xs_dict_append(ins, "rules", xs_stock(XSTYPE_LIST));
2714
2715        xs *l1 = xs_list_append(xs_list_new(), "en");
2716        ins = xs_dict_append(ins, "languages", l1);
2717
2718        xs *wss = xs_fmt("wss:/" "/%s", xs_dict_get(srv_config, "host"));
2719        xs *urls = xs_dict_new();
2720        urls = xs_dict_append(urls, "streaming_api", wss);
2721
2722        ins = xs_dict_append(ins, "urls", urls);
2723
2724        xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock(0));
2725        d2 = xs_dict_append(d2, "status_count", xs_stock(0));
2726        d2 = xs_dict_append(d2, "domain_count", xs_stock(0));
2727        ins = xs_dict_append(ins, "stats", d2);
2728
2729        ins = xs_dict_append(ins, "registrations",     xs_stock(XSTYPE_FALSE));
2730        ins = xs_dict_append(ins, "approval_required", xs_stock(XSTYPE_FALSE));
2731        ins = xs_dict_append(ins, "invites_enabled",   xs_stock(XSTYPE_FALSE));
2732
2733        xs *cfg = xs_dict_new();
2734
2735        {
2736            xs *d11 = xs_json_loads("{\"characters_reserved_per_url\":32,"
2737                "\"max_characters\":100000,\"max_media_attachments\":4}");
2738
2739            const xs_number *max_attachments = xs_dict_get(srv_config, "max_attachments");
2740            if (xs_type(max_attachments) == XSTYPE_NUMBER)
2741                d11 = xs_dict_set(d11, "max_media_attachments", max_attachments);
2742
2743            cfg = xs_dict_append(cfg, "statuses", d11);
2744
2745            xs *d12 = xs_json_loads("{\"max_featured_tags\":0}");
2746            cfg = xs_dict_append(cfg, "accounts", d12);
2747
2748            xs *d13 = xs_json_loads("{\"image_matrix_limit\":33177600,"
2749                        "\"image_size_limit\":16777216,"
2750                        "\"video_frame_rate_limit\":120,"
2751                        "\"video_matrix_limit\":8294400,"
2752                        "\"video_size_limit\":103809024}"
2753            );
2754
2755            {
2756                /* get the supported mime types from the internal list */
2757                const char **p = xs_mime_types;
2758                xs_set mtypes;
2759                xs_set_init(&mtypes);
2760
2761                while (*p) {
2762                    const char *type = p[1];
2763
2764                    if (xs_startswith(type, "image/") ||
2765                        xs_startswith(type, "video/") ||
2766                        xs_startswith(type, "audio/"))
2767                        xs_set_add(&mtypes, type);
2768
2769                    p += 2;
2770                }
2771
2772                xs *l = xs_set_result(&mtypes);
2773                d13 = xs_dict_append(d13, "supported_mime_types", l);
2774            }
2775
2776            cfg = xs_dict_append(cfg, "media_attachments", d13);
2777
2778            xs *d14 = xs_json_loads("{\"max_characters_per_option\":50,"
2779                "\"max_expiration\":2629746,"
2780                "\"max_options\":8,\"min_expiration\":300}");
2781            cfg = xs_dict_append(cfg, "polls", d14);
2782
2783
2784            xs *d15 = xs_json_loads("{\"max_reactions\":50}");
2785                cfg = xs_dict_append(cfg, "reactions", d15);
2786
2787        }
2788
2789        ins = xs_dict_append(ins, "configuration", cfg);
2790
2791        const char *admin_account = xs_dict_get(srv_config, "admin_account");
2792
2793        if (!xs_is_null(admin_account) && *admin_account) {
2794            snac admin;
2795
2796            if (user_open(&admin, admin_account)) {
2797                xs *actor = msg_actor(&admin);
2798                xs *acct  = mastoapi_account(NULL, actor);
2799
2800                ins = xs_dict_append(ins, "contact_account", acct);
2801
2802                user_free(&admin);
2803            }
2804        }
2805
2806        *body  = xs_json_dumps(ins, 4);
2807        *ctype = "application/json";
2808        status = HTTP_STATUS_OK;
2809    }
2810    else
2811    if (strcmp(cmd, "/v1/instance/peers") == 0) { /** **/
2812        /* get the collected inbox list as the instances "this domain is aware of" */
2813        xs *list = inbox_list();
2814        xs *peers = xs_list_new();
2815        const char *inbox;
2816
2817        xs_list_foreach(list, inbox) {
2818            xs *l = xs_split(inbox, "/");
2819            const char *domain = xs_list_get(l, 2);
2820
2821            if (xs_is_string(domain))
2822                peers = xs_list_append(peers, domain);
2823        }
2824
2825        *body  = xs_json_dumps(peers, 4);
2826        *ctype = "application/json";
2827        status = HTTP_STATUS_OK;
2828    }
2829    else
2830    if (strcmp(cmd, "/v1/instance/extended_description") == 0) { /** **/
2831        xs *d = xs_dict_new();
2832        xs *greeting = xs_fmt("%s/greeting.html", srv_basedir);
2833        time_t t = mtime(greeting);
2834        xs *updated_at = xs_str_iso_date(t);
2835        xs *content = xs_replace(snac_blurb, "%host%", xs_dict_get(srv_config, "host"));
2836
2837        d = xs_dict_set(d, "updated_at", updated_at);
2838        d = xs_dict_set(d, "content", content);
2839
2840        *body  = xs_json_dumps(d, 4);
2841        *ctype = "application/json";
2842        status = HTTP_STATUS_OK;
2843    }
2844    else
2845    if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
2846        /* information about a status */
2847        if (logged_in) {
2848            xs *l = xs_split(cmd, "/");
2849            const char *oid = xs_list_get(l, 3);
2850            const char *op = xs_list_get(l, 4);
2851
2852            if (!xs_is_null(oid)) {
2853                xs *msg = NULL;
2854                xs *out = NULL;
2855
2856                /* skip the 'fake' part of the id */
2857                oid = MID_TO_MD5(oid);
2858
2859                xs *id = xs_tolower_i(xs_dup(oid));
2860
2861                if (valid_status(object_get_by_md5(id, &msg))) {
2862                    if (op == NULL) {
2863                        if (!is_muted(&snac1, get_atto(msg))) {
2864                            /* return the status itself */
2865                            out = mastoapi_status(&snac1, msg);
2866                        }
2867                    }
2868                    else
2869                    if (strcmp(op, "context") == 0) { /** **/
2870                        /* return ancestors and children */
2871                        xs *anc = xs_list_new();
2872                        xs *des = xs_list_new();
2873                        char pid[MD5_HEX_SIZE];
2874
2875                        /* build the [grand]parent list, moving up */
2876                        strncpy(pid, id, sizeof(pid));
2877
2878                        while (object_parent(pid, pid)) {
2879                            xs *m2 = NULL;
2880
2881                            if (valid_status(timeline_get_by_md5(&snac1, pid, &m2))) {
2882                                xs *st = mastoapi_status(&snac1, m2);
2883
2884                                if (st)
2885                                    anc = xs_list_insert(anc, 0, st);
2886                            }
2887                            else
2888                                break;
2889                        }
2890
2891                        /* build the children list */
2892                        xs *childs = build_childrens(msg, &snac1);
2893                        if (xs_list_len(childs) > 0)
2894                            des = xs_list_cat(des, childs);
2895
2896                        out = xs_dict_new();
2897                        out = xs_dict_append(out, "ancestors",   anc);
2898                        out = xs_dict_append(out, "descendants", des);
2899                    }
2900                    else
2901                    if (strcmp(op, "reblogged_by") == 0 || /** **/
2902                        strcmp(op, "favourited_by") == 0) { /** **/
2903                        /* return the list of people who liked or boosted this */
2904                        out = xs_list_new();
2905
2906                        xs *l = NULL;
2907
2908                        if (op[0] == 'r')
2909                            l = object_announces(xs_dict_get(msg, "id"));
2910                        else
2911                            l = object_likes(xs_dict_get(msg, "id"));
2912
2913                        xs_list *p = l;
2914                        const xs_str *v;
2915
2916                        while (xs_list_iter(&p, &v)) {
2917                            xs *actor2 = NULL;
2918
2919                            if (valid_status(object_get_by_md5(v, &actor2))) {
2920                                xs *acct2 = mastoapi_account(&snac1, actor2);
2921
2922                                out = xs_list_append(out, acct2);
2923                            }
2924                        }
2925                    }
2926                    else
2927                    if (strcmp(op, "source") == 0) { /** **/
2928                        out = xs_dict_new();
2929
2930                        /* get the mastoapi status id */
2931                        out = xs_dict_append(out, "id", xs_list_get(l, 3));
2932
2933                        out = xs_dict_append(out, "text", xs_dict_get(msg, "sourceContent"));
2934                        out = xs_dict_append(out, "spoiler_text", xs_dict_get(msg, "summary"));
2935                    }
2936                }
2937                else
2938                    srv_debug(1, xs_fmt("mastoapi status: bad id %s", id));
2939
2940                if (out != NULL) {
2941                    *body  = xs_json_dumps(out, 4);
2942                    *ctype = "application/json";
2943                    status = HTTP_STATUS_OK;
2944                }
2945            }
2946        }
2947        else
2948            status = HTTP_STATUS_UNAUTHORIZED;
2949    }
2950    else
2951    if (strcmp(cmd, "/v1/preferences") == 0) { /** **/
2952        /* TBD */
2953        *body  = xs_dup("{}");
2954        *ctype = "application/json";
2955        status = HTTP_STATUS_OK;
2956    }
2957    else
2958    if (strcmp(cmd, "/v1/markers") == 0) { /** **/
2959        if (logged_in) {
2960            const xs_list *timeline = xs_dict_get(args, "timeline[]");
2961            xs_str *json = NULL;
2962            if (!xs_is_null(timeline))
2963                json = xs_json_dumps(markers_get(&snac1, timeline), 4);
2964
2965            if (!xs_is_null(json))
2966                *body = json;
2967            else
2968                *body = xs_dup("{}");
2969
2970            *ctype = "application/json";
2971            status = HTTP_STATUS_OK;
2972        }
2973        else
2974            status = HTTP_STATUS_UNAUTHORIZED;
2975    }
2976    else
2977    if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
2978        if (logged_in) {
2979            xs *r = xs_list_new();
2980            const xs_list *followed_hashtags = xs_dict_get_def(snac1.config,
2981                        "followed_hashtags", xs_stock(XSTYPE_LIST));
2982            const char *hashtag;
2983
2984            xs_list_foreach(followed_hashtags, hashtag) {
2985                if (*hashtag == '#') {
2986                    xs *d = xs_dict_new();
2987                    xs *s = xs_fmt("%s?t=%s", srv_baseurl, hashtag + 1);
2988
2989                    d = xs_dict_set(d, "name", hashtag + 1);
2990                    d = xs_dict_set(d, "url", s);
2991                    d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST));
2992
2993                    r = xs_list_append(r, d);
2994                }
2995            }
2996
2997            *body  = xs_json_dumps(r, 4);
2998            *ctype = "application/json";
2999            status = HTTP_STATUS_OK;
3000        }
3001        else
3002            status = HTTP_STATUS_UNAUTHORIZED;
3003    }
3004    else
3005    if (strcmp(cmd, "/v1/blocks") == 0) { /** **/
3006        *body  = xs_dup("[]");
3007        *ctype = "application/json";
3008        status = HTTP_STATUS_OK;
3009    }
3010    else
3011    if (strcmp(cmd, "/v1/mutes") == 0) { /** **/
3012        *body  = xs_dup("[]");
3013        *ctype = "application/json";
3014        status = HTTP_STATUS_OK;
3015    }
3016    else
3017    if (strcmp(cmd, "/v1/trends/tags") == 0) { /** **/
3018        *body  = xs_dup("[]");
3019        *ctype = "application/json";
3020        status = HTTP_STATUS_OK;
3021    }
3022    else
3023    if (strcmp(cmd, "/v1/trends/statuses") == 0) { /** **/
3024        *body  = xs_dup("[]");
3025        *ctype = "application/json";
3026        status = HTTP_STATUS_OK;
3027    }
3028    else
3029    if (strcmp(cmd, "/v2/search") == 0) { /** **/
3030        if (logged_in) {
3031            const char *q      = xs_dict_get(args, "q");
3032            const char *type   = xs_dict_get(args, "type");
3033            const char *offset = xs_dict_get(args, "offset");
3034
3035            xs *acl = xs_list_new();
3036            xs *stl = xs_list_new();
3037            xs *htl = xs_list_new();
3038            xs *res = xs_dict_new();
3039
3040            if (xs_is_null(offset) || strcmp(offset, "0") == 0) {
3041                /* reply something only for offset 0; otherwise,
3042                   apps like Tusky keep asking again and again */
3043                if (xs_startswith(q, "https://")) {
3044                    if (!timeline_here(&snac1, q)) {
3045                        xs *object = NULL;
3046                        int status;
3047
3048                        status = activitypub_request(&snac1, q, &object);
3049                        snac_debug(&snac1, 1, xs_fmt("Request searched URL %s %d", q, status));
3050
3051                        if (valid_status(status)) {
3052                            /* got it; also request the actor */
3053                            const char *attr_to = get_atto(object);
3054                            xs *actor_obj = NULL;
3055
3056                            if (!xs_is_null(attr_to)) {
3057                                status = actor_request(&snac1, attr_to, &actor_obj);
3058
3059                                snac_debug(&snac1, 1, xs_fmt("Request author %s of %s %d", attr_to, q, status));
3060
3061                                if (valid_status(status)) {
3062                                    /* add the actor */
3063                                    actor_add(attr_to, actor_obj);
3064
3065                                    /* add the post to the timeline */
3066                                    timeline_add(&snac1, q, object);
3067                                }
3068                            }
3069                        }
3070                    }
3071                }
3072
3073                if (!xs_is_null(q)) {
3074                    if (xs_is_null(type) || strcmp(type, "accounts") == 0) {
3075                        /* do a webfinger query */
3076                        char *actor = NULL;
3077                        char *user  = NULL;
3078
3079                        if (valid_status(webfinger_request(q, &actor, &user)) && actor) {
3080                            xs *actor_o = NULL;
3081
3082                            if (valid_status(actor_request(&snac1, actor, &actor_o))) {
3083                                xs *acct = mastoapi_account(NULL, actor_o);
3084
3085                                acl = xs_list_append(acl, acct);
3086
3087                                if (!object_here(actor))
3088                                    object_add(actor, actor_o);
3089                            }
3090                        }
3091                    }
3092
3093                    if (xs_is_null(type) || strcmp(type, "hashtags") == 0) {
3094                        /* search this tag */
3095                        xs *tl = tag_search((char *)q, 0, 1);
3096
3097                        if (xs_list_len(tl)) {
3098                            xs *d = xs_dict_new();
3099
3100                            d = xs_dict_append(d, "name", q);
3101                            xs *url = xs_fmt("%s?t=%s", srv_baseurl, q);
3102                            d = xs_dict_append(d, "url", url);
3103                            d = xs_dict_append(d, "history", xs_stock(XSTYPE_LIST));
3104
3105                            htl = xs_list_append(htl, d);
3106                        }
3107                    }
3108
3109                    if (xs_is_null(type) || strcmp(type, "statuses") == 0) {
3110                        int to = 0;
3111                        int cnt = 40;
3112                        xs *tl = content_search(&snac1, q, 1, 0, cnt, 0, &to);
3113                        int c = 0;
3114                        const char *v;
3115
3116                        while (xs_list_next(tl, &v, &c) && --cnt) {
3117                            xs *post = NULL;
3118
3119                            if (!valid_status(timeline_get_by_md5(&snac1, v, &post)))
3120                                continue;
3121
3122                            xs *s = mastoapi_status(&snac1, post);
3123
3124                            if (!xs_is_null(s))
3125                                stl = xs_list_append(stl, s);
3126                        }
3127                    }
3128                }
3129            }
3130
3131            res = xs_dict_append(res, "accounts", acl);
3132            res = xs_dict_append(res, "statuses", stl);
3133            res = xs_dict_append(res, "hashtags", htl);
3134
3135            *body  = xs_json_dumps(res, 4);
3136            *ctype = "application/json";
3137            status = HTTP_STATUS_OK;
3138        }
3139        else
3140            status = HTTP_STATUS_UNAUTHORIZED;
3141    }
3142
3143    /* user cleanup */
3144    if (logged_in)
3145        user_free(&snac1);
3146
3147    srv_debug(1, xs_fmt("mastoapi_get_handler %s %d", q_path, status));
3148
3149    return status;
3150}
3151
3152
3153int mastoapi_post_handler(const xs_dict *req, const char *q_path,
3154                          const char *payload, int p_size,
3155                          char **body, int *b_size, char **ctype)
3156{
3157    (void)p_size;
3158    (void)b_size;
3159
3160    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
3161        return 0;
3162
3163    int status    = HTTP_STATUS_NOT_FOUND;
3164    xs *args      = NULL;
3165    const char *i_ctype = xs_dict_get(req, "content-type");
3166
3167    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
3168        if (!xs_is_null(payload)) {
3169            args = xs_json_loads(payload);
3170        }
3171    }
3172    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
3173    {
3174        // Some apps send form data instead of json so we should cater for those
3175        if (!xs_is_null(payload)) {
3176            args    = xs_url_vars(payload);
3177        }
3178    }
3179    else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data"))
3180    {
3181        // Handle multipart/form-data by using p_vars (already parsed by httpd)
3182        args = xs_dup(xs_dict_get(req, "p_vars"));
3183    }
3184
3185    /* if args still NULL, try falling back to p_vars or q_vars */
3186    if (args == NULL)
3187        args = xs_dup(xs_dict_get(req, "p_vars"));
3188
3189    if (args == NULL)
3190        args = xs_dup(xs_dict_get(req, "q_vars"));
3191
3192    if (args == NULL) {
3193        srv_debug(1, xs_fmt("mastoapi_post_handler: failed to parse args for %s, content-type: %s",
3194                           q_path, i_ctype ? i_ctype : "(null)"));
3195        return HTTP_STATUS_BAD_REQUEST;
3196    }
3197
3198    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
3199
3200    snac snac = {0};
3201    int logged_in = process_auth_token(&snac, req);
3202
3203    if (strcmp(cmd, "/v1/apps") == 0) { /** **/
3204        const char *name  = xs_dict_get(args, "client_name");
3205        const char *ruri  = xs_dict_get(args, "redirect_uris");
3206        const char *scope = xs_dict_get(args, "scope");
3207
3208        /* Ice Cubes sends these values as query parameters, so try these */
3209        if (name == NULL && ruri == NULL && scope == NULL) {
3210            xs_free(args);
3211            args = xs_dup(xs_dict_get(req, "q_vars"));
3212            name  = xs_dict_get(args, "client_name");
3213            ruri  = xs_dict_get(args, "redirect_uris");
3214            scope = xs_dict_get(args, "scope");
3215        }
3216
3217        if (xs_type(ruri) == XSTYPE_LIST)
3218            ruri = xs_dict_get(ruri, 0);
3219
3220        if (name && ruri) {
3221            xs *app  = xs_dict_new();
3222            xs *id   = xs_replace_i(tid(0), ".", "");
3223            xs *csec = random_str();
3224            xs *vkey = xs_dup(xs_dict_get(srv_config, "vkey"));
3225            if (vkey == NULL)
3226                vkey = random_str();
3227
3228            xs *cid  = NULL;
3229
3230            /* pick a non-existent random cid */
3231            for (;;) {
3232                cid = random_str();
3233                xs *p_app = app_get(cid);
3234
3235                if (p_app == NULL)
3236                    break;
3237
3238                xs_free(cid);
3239            }
3240
3241            app = xs_dict_append(app, "name",          name);
3242            app = xs_dict_append(app, "redirect_uri",  ruri);
3243            app = xs_dict_append(app, "client_id",     cid);
3244            app = xs_dict_append(app, "client_secret", csec);
3245            app = xs_dict_append(app, "vapid_key",     vkey);
3246            app = xs_dict_append(app, "id",            id);
3247            app = xs_dict_append(app, "client_secret_expires_at", xs_stock(0));
3248
3249            *body  = xs_json_dumps(app, 4);
3250            *ctype = "application/json";
3251            status = HTTP_STATUS_OK;
3252
3253            app = xs_dict_append(app, "code", "");
3254
3255            if (scope)
3256                app = xs_dict_append(app, "scope", scope);
3257
3258            app_add(cid, app);
3259
3260            srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
3261        }
3262    }
3263    else
3264    if (strcmp(cmd, "/v1/statuses") == 0) { /** **/
3265        if (logged_in) {
3266            /* post a new Note */
3267            const char *content          = xs_dict_get(args, "status");
3268            const char *mid              = xs_dict_get(args, "in_reply_to_id");
3269            const char *visibility       = xs_dict_get(args, "visibility");
3270            const char *summary          = xs_dict_get(args, "spoiler_text");
3271            const char *poll_opts        = xs_dict_get(args, "poll[options][]");
3272            const char *poll_end_secs    = xs_dict_get(args, "poll[expires_in]");
3273            const char *poll_multiple    = xs_dict_get(args, "poll[multiple]");
3274            // const char *poll_hide_totals = xs_dict_get(args, "poll[hide_totals]");
3275            const char *media_ids        = xs_dict_get(args, "media_ids");
3276            const char *language         = xs_dict_get(args, "language");
3277
3278            if (xs_is_null(media_ids))
3279                media_ids = xs_dict_get(args, "media_ids[]");
3280
3281            if (xs_is_null(media_ids))
3282                media_ids = xs_dict_get(args, "media_ids");
3283
3284            if (xs_is_null(visibility))
3285                visibility = "public";
3286
3287            xs *attach_list = xs_list_new();
3288            xs *irt         = NULL;
3289
3290            /* is it a reply? */
3291            if (mid != NULL) {
3292                xs *r_msg = NULL;
3293                const char *md5 = MID_TO_MD5(mid);
3294
3295                if (valid_status(object_get_by_md5(md5, &r_msg)))
3296                    irt = xs_dup(xs_dict_get(r_msg, "id"));
3297            }
3298
3299            /* does it have attachments (and no poll)? */
3300            if (!xs_is_null(media_ids) && xs_is_null(poll_end_secs) && xs_is_null(poll_opts)) {
3301                xs *mi = NULL;
3302
3303                if (xs_type(media_ids) == XSTYPE_LIST)
3304                    mi = xs_dup(media_ids);
3305                else {
3306                    mi = xs_list_new();
3307                    mi = xs_list_append(mi, media_ids);
3308                }
3309
3310                xs_list *p = mi;
3311                const xs_str *v;
3312
3313                while (xs_list_iter(&p, &v)) {
3314                    xs *l    = xs_list_new();
3315                    xs *url  = xs_fmt("%s/s/%s", snac.actor, v);
3316                    xs *desc = static_get_meta(&snac, v);
3317
3318                    l = xs_list_append(l, url);
3319                    l = xs_list_append(l, desc);
3320
3321                    attach_list = xs_list_append(attach_list, l);
3322                }
3323            }
3324
3325            /* prepare the message */
3326            xs *msg = NULL;
3327
3328            int scope = SCOPE_MENTIONED;
3329            if (strcmp(visibility, "unlisted") == 0)
3330                scope = SCOPE_UNLISTED;
3331            else
3332            if (strcmp(visibility, "public") == 0)
3333                scope = SCOPE_PUBLIC;
3334            else
3335            if (strcmp(visibility, "private") == 0)
3336                scope = SCOPE_FOLLOWERS;
3337
3338            /* does it have a poll? */
3339            if (!xs_is_null(poll_opts) && !xs_is_null(poll_end_secs)) {
3340                xs *po = NULL;
3341                int end_secs = atoi(poll_end_secs);
3342                int multiple = 0;
3343
3344                if (xs_type(poll_opts) == XSTYPE_LIST)
3345                    po = xs_dup(poll_opts);
3346                else {
3347                    po = xs_list_new();
3348                    po = xs_list_append(po, poll_opts);
3349                }
3350
3351                if (!xs_is_null(poll_multiple) && strcmp(poll_multiple, "true") == 0)
3352                    multiple = 1;
3353
3354                msg = msg_question(&snac, content, attach_list,
3355                                   poll_opts, multiple, end_secs);
3356
3357                enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
3358            }
3359            else
3360                msg = msg_note(&snac, content, NULL, irt, attach_list, scope, language, NULL);
3361
3362            if (!xs_is_null(summary) && *summary) {
3363                msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
3364                msg = xs_dict_set(msg, "summary",   summary);
3365            }
3366
3367            /* scheduled? */
3368            const char *scheduled_at = xs_dict_get(args, "scheduled_at");
3369
3370            if (xs_is_string(scheduled_at) && *scheduled_at) {
3371                msg = xs_dict_set(msg, "published", scheduled_at);
3372
3373                schedule_add(&snac, xs_dict_get(msg, "id"), msg);
3374            }
3375            else {
3376                /* store */
3377                timeline_add(&snac, xs_dict_get(msg, "id"), msg);
3378
3379                /* 'Create' message */
3380                xs *c_msg = msg_create(&snac, msg);
3381                enqueue_message(&snac, c_msg);
3382
3383                timeline_touch(&snac);
3384            }
3385
3386            /* convert to a mastodon status as a response code */
3387            xs *st = mastoapi_status(&snac, msg);
3388
3389            *body  = xs_json_dumps(st, 4);
3390            *ctype = "application/json";
3391            status = HTTP_STATUS_OK;
3392        }
3393        else
3394            status = HTTP_STATUS_UNAUTHORIZED;
3395    }
3396    else
3397    if (xs_startswith(cmd, "/v1/statuses")) { /** **/
3398        if (logged_in) {
3399            /* operations on a status */
3400            xs *l = xs_split(cmd, "/");
3401            const char *mid = xs_list_get(l, 3);
3402            const char *op  = xs_list_get(l, 4);
3403
3404            if (!xs_is_null(mid)) {
3405                xs *msg = NULL;
3406                xs *out = NULL;
3407
3408                /* skip the 'fake' part of the id */
3409                mid = MID_TO_MD5(mid);
3410
3411                /* try timeline first, then global object store for remote posts */
3412                int found = valid_status(timeline_get_by_md5(&snac, mid, &msg));
3413                if (!found)
3414                    found = valid_status(object_get_by_md5(mid, &msg));
3415
3416                if (found) {
3417                    const char *id = xs_dict_get(msg, "id");
3418
3419                    if (op == NULL) {
3420                        /* no operation (?) */
3421                    }
3422                    else
3423                    if (strcmp(op, "favourite") == 0) { /** **/
3424                        xs *n_msg = msg_admiration(&snac, id, "Like");
3425
3426                        if (n_msg != NULL) {
3427                            enqueue_message(&snac, n_msg);
3428                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1, msg);
3429
3430                            out = mastoapi_status(&snac, msg);
3431                        }
3432                    }
3433                    else
3434                    if (strcmp(op, "unfavourite") == 0) { /** **/
3435                        xs *n_msg = msg_repulsion(&snac, id, "Like");
3436
3437                        if (n_msg != NULL) {
3438                            enqueue_message(&snac, n_msg);
3439
3440                            out = mastoapi_status(&snac, msg);
3441                        }
3442                    }
3443                    else
3444                    if (strcmp(op, "react") == 0) { /** **/
3445                        const char *eid = xs_list_get(l, 5);
3446                        xs *n_msg = msg_emoji_init(&snac, id, eid);
3447                        if (n_msg)
3448                            out = mastoapi_status(&snac, n_msg);
3449                    }
3450                    else
3451                    if (strcmp(op, "unreact") == 0) { /** **/
3452                        const char *eid = xs_list_get(l, 5);
3453                        xs *content = xs_fmt("%s", eid);
3454
3455                        if (eid) {
3456                            xs *n_msg = msg_emoji_unreact(&snac, id, content);
3457
3458                            if (n_msg != NULL) {
3459                                enqueue_message(&snac, n_msg);
3460
3461                                out = mastoapi_status(&snac, msg);
3462                            }
3463                        }
3464                    }
3465
3466                    else
3467                    if (strcmp(op, "reblog") == 0) { /** **/
3468                        xs *n_msg = msg_admiration(&snac, id, "Announce");
3469
3470                        if (n_msg != NULL) {
3471                            enqueue_message(&snac, n_msg);
3472                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0, msg);
3473
3474                            out = mastoapi_status(&snac, msg);
3475                        }
3476                    }
3477                    else
3478                    if (strcmp(op, "unreblog") == 0) { /** **/
3479                        xs *n_msg = msg_repulsion(&snac, id, "Announce");
3480
3481                        if (n_msg != NULL) {
3482                            enqueue_message(&snac, n_msg);
3483
3484                            out = mastoapi_status(&snac, msg);
3485                        }
3486                    }
3487                    else
3488                    if (strcmp(op, "bookmark") == 0) { /** **/
3489                        /* bookmark this message */
3490                        if (bookmark(&snac, id) == 0)
3491                            out = mastoapi_status(&snac, msg);
3492                        else
3493                            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3494                    }
3495                    else
3496                    if (strcmp(op, "unbookmark") == 0) { /** **/
3497                        /* unbookmark this message */
3498                        unbookmark(&snac, id);
3499                        out = mastoapi_status(&snac, msg);
3500                    }
3501                    else
3502                    if (strcmp(op, "pin") == 0) { /** **/
3503                        /* pin this message */
3504                        if (pin(&snac, id) == 0)
3505                            out = mastoapi_status(&snac, msg);
3506                        else
3507                            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3508                    }
3509                    else
3510                    if (strcmp(op, "unpin") == 0) { /** **/
3511                        /* unpin this message */
3512                        unpin(&snac, id);
3513                        out = mastoapi_status(&snac, msg);
3514                    }
3515                    else
3516                    if (strcmp(op, "mute") == 0) { /** **/
3517                        /* Mastodon's mute is snac's hide */
3518                    }
3519                    else
3520                    if (strcmp(op, "unmute") == 0) { /** **/
3521                        /* Mastodon's unmute is snac's unhide */
3522                    }
3523                }
3524
3525                if (out != NULL) {
3526                    *body  = xs_json_dumps(out, 4);
3527                    *ctype = "application/json";
3528                    status = HTTP_STATUS_OK;
3529                }
3530            }
3531        }
3532        else
3533            status = HTTP_STATUS_UNAUTHORIZED;
3534    }
3535    else
3536    if (strcmp(cmd, "/v1/notifications/clear") == 0) { /** **/
3537        if (logged_in) {
3538            notify_clear(&snac);
3539            timeline_touch(&snac);
3540
3541            *body  = xs_dup("{}");
3542            *ctype = "application/json";
3543            status = HTTP_STATUS_OK;
3544        }
3545        else
3546            status = HTTP_STATUS_UNAUTHORIZED;
3547    }
3548    else
3549    if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
3550        /* I don't know what I'm doing */
3551        if (logged_in) {
3552            const char *v;
3553
3554            xs *wpush = xs_dict_new();
3555
3556            wpush = xs_dict_append(wpush, "id", "1");
3557
3558            v = xs_dict_get(args, "data");
3559            v = xs_dict_get(v, "alerts");
3560            wpush = xs_dict_append(wpush, "alerts", v);
3561
3562            v = xs_dict_get(args, "subscription");
3563            v = xs_dict_get(v, "endpoint");
3564            wpush = xs_dict_append(wpush, "endpoint", v);
3565
3566            xs *server_key = random_str();
3567            wpush = xs_dict_append(wpush, "server_key", server_key);
3568
3569            *body  = xs_json_dumps(wpush, 4);
3570            *ctype = "application/json";
3571            status = HTTP_STATUS_OK;
3572        }
3573        else
3574            status = HTTP_STATUS_UNAUTHORIZED;
3575    }
3576    else
3577    if (strcmp(cmd, "/v1/media") == 0 || strcmp(cmd, "/v2/media") == 0) { /** **/
3578        if (logged_in) {
3579            const xs_list *file = xs_dict_get(args, "file");
3580            const char *desc    = xs_dict_get(args, "description");
3581
3582            if (xs_is_null(desc))
3583                desc = "";
3584
3585            status = HTTP_STATUS_BAD_REQUEST;
3586
3587            if (xs_type(file) == XSTYPE_LIST) {
3588                const char *fn = xs_list_get(file, 0);
3589
3590                if (*fn != '\0') {
3591                    char *ext = strrchr(fn, '.');
3592                    char rnd[32];
3593                    xs_rnd_buf(rnd, sizeof(rnd));
3594                    xs *hash  = xs_md5_hex(rnd, sizeof(rnd));
3595                    xs *id    = xs_fmt("post-%s%s", hash, ext ? ext : "");
3596                    xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
3597                    int fo    = xs_number_get(xs_list_get(file, 1));
3598                    int fs    = xs_number_get(xs_list_get(file, 2));
3599
3600                    /* store */
3601                    static_put(&snac, id, payload + fo, fs);
3602                    static_put_meta(&snac, id, desc);
3603
3604                    /* prepare a response */
3605                    xs *rsp = xs_dict_new();
3606
3607                    rsp = xs_dict_append(rsp, "id",          id);
3608                    rsp = xs_dict_append(rsp, "type",        "image");
3609                    rsp = xs_dict_append(rsp, "url",         url);
3610                    rsp = xs_dict_append(rsp, "preview_url", url);
3611                    rsp = xs_dict_append(rsp, "remote_url",  url);
3612                    rsp = xs_dict_append(rsp, "description", desc);
3613
3614                    *body  = xs_json_dumps(rsp, 4);
3615                    *ctype = "application/json";
3616                    status = HTTP_STATUS_OK;
3617                }
3618            }
3619        }
3620        else
3621            status = HTTP_STATUS_UNAUTHORIZED;
3622    }
3623    else
3624    if (xs_startswith(cmd, "/v1/accounts")) { /** **/
3625        if (logged_in) {
3626            /* account-related information */
3627            xs *l = xs_split(cmd, "/");
3628            const char *md5 = xs_list_get(l, 3);
3629            const char *opt = xs_list_get(l, 4);
3630            xs *rsp = NULL;
3631
3632            if (!xs_is_null(md5) && *md5) {
3633                xs *actor_o = NULL;
3634
3635                if (xs_is_null(opt)) {
3636                    /* ? */
3637                }
3638                else
3639                if (strcmp(opt, "follow") == 0) { /** **/
3640                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
3641                        const char *actor = xs_dict_get(actor_o, "id");
3642
3643                        xs *msg = msg_follow(&snac, actor);
3644
3645                        if (msg != NULL) {
3646                            /* reload the actor from the message, in may be different */
3647                            actor = xs_dict_get(msg, "object");
3648
3649                            following_add(&snac, actor, msg);
3650
3651                            enqueue_output_by_actor(&snac, msg, actor, 0);
3652
3653                            rsp = mastoapi_relationship(&snac, md5);
3654                        }
3655                    }
3656                }
3657                else
3658                if (strcmp(opt, "unfollow") == 0) { /** **/
3659                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
3660                        const char *actor = xs_dict_get(actor_o, "id");
3661
3662                        /* get the following object */
3663                        xs *object = NULL;
3664
3665                        if (valid_status(following_get(&snac, actor, &object))) {
3666                            xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
3667
3668                            following_del(&snac, actor);
3669
3670                            enqueue_output_by_actor(&snac, msg, actor, 0);
3671
3672                            rsp = mastoapi_relationship(&snac, md5);
3673                        }
3674                    }
3675                }
3676                else
3677                if (strcmp(opt, "block") == 0) { /** **/
3678                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
3679                        const char *actor = xs_dict_get(actor_o, "id");
3680
3681                        mute(&snac, actor);
3682
3683                        rsp = mastoapi_relationship(&snac, md5);
3684                    }
3685                }
3686                else
3687                if (strcmp(opt, "unblock") == 0) { /** **/
3688                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
3689                        const char *actor = xs_dict_get(actor_o, "id");
3690
3691                        unmute(&snac, actor);
3692
3693                        rsp = mastoapi_relationship(&snac, md5);
3694                    }
3695                }
3696            }
3697
3698            if (rsp != NULL) {
3699                *body  = xs_json_dumps(rsp, 4);
3700                *ctype = "application/json";
3701                status = HTTP_STATUS_OK;
3702            }
3703        }
3704        else
3705            status = HTTP_STATUS_UNAUTHORIZED;
3706    }
3707    else
3708    if (xs_startswith(cmd, "/v1/polls")) { /** **/
3709        if (logged_in) {
3710            /* operations on a status */
3711            xs *l = xs_split(cmd, "/");
3712            const char *mid = xs_list_get(l, 3);
3713            const char *op  = xs_list_get(l, 4);
3714
3715            if (!xs_is_null(mid)) {
3716                xs *msg = NULL;
3717                xs *out = NULL;
3718
3719                /* skip the 'fake' part of the id */
3720                mid = MID_TO_MD5(mid);
3721
3722                /* try timeline first, then global object store for remote posts */
3723                int found = valid_status(timeline_get_by_md5(&snac, mid, &msg));
3724                if (!found)
3725                    found = valid_status(object_get_by_md5(mid, &msg));
3726
3727                if (found) {
3728                    const char *id   = xs_dict_get(msg, "id");
3729                    const char *atto = get_atto(msg);
3730                    int closed = 0;
3731                    const char *f_closed = NULL;
3732
3733                    if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
3734                        /* it has a closed date... but is it in the past? */
3735                        time_t t0 = time(NULL);
3736                        time_t t1 = xs_parse_iso_date(f_closed, 0);
3737
3738                        if (t1 < t0)
3739                            closed = 1;
3740                    }
3741
3742                    if (closed || was_question_voted(&snac, id))
3743                        status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3744                    else {
3745                        const xs_list *opts = xs_dict_get(msg, "oneOf");
3746                        if (opts == NULL)
3747                            opts = xs_dict_get(msg, "anyOf");
3748
3749                        if (op == NULL) {
3750                        }
3751                        else {
3752                            if (strcmp(op, "votes") == 0) {
3753                                const xs_list *choices = xs_dict_get(args, "choices[]");
3754
3755                                if (xs_is_null(choices))
3756                                    choices = xs_dict_get(args, "choices");
3757
3758                                if (xs_type(choices) == XSTYPE_LIST) {
3759                                    const xs_str *v;
3760
3761                                    int c = 0;
3762                                    while (xs_list_next(choices, &v, &c)) {
3763                                        int io           = atoi(v);
3764                                        const xs_dict *o = xs_list_get(opts, io);
3765
3766                                        if (o) {
3767                                            const char *name = xs_dict_get(o, "name");
3768
3769                                            xs *msg = msg_note(&snac, "", atto, (char *)id, NULL, 1, NULL, NULL);
3770                                            msg = xs_dict_append(msg, "name", name);
3771
3772                                            xs *c_msg = msg_create(&snac, msg);
3773                                            enqueue_message(&snac, c_msg);
3774                                            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
3775                                        }
3776                                    }
3777
3778                                    out = mastoapi_poll(&snac, msg);
3779                                }
3780                            }
3781                        }
3782                    }
3783                }
3784
3785                if (out != NULL) {
3786                    *body  = xs_json_dumps(out, 4);
3787                    *ctype = "application/json";
3788                    status = HTTP_STATUS_OK;
3789                }
3790            }
3791        }
3792        else
3793            status = HTTP_STATUS_UNAUTHORIZED;
3794    }
3795    else
3796    if (strcmp(cmd, "/v1/lists") == 0) {
3797        if (logged_in) {
3798            const char *title = xs_dict_get(args, "title");
3799
3800            if (xs_type(title) == XSTYPE_STRING) {
3801                /* add the list */
3802                xs *out = xs_dict_new();
3803                xs *lid = list_maint(&snac, title, 1);
3804
3805                if (!xs_is_null(lid)) {
3806                    out = xs_dict_append(out, "id", lid);
3807                    out = xs_dict_append(out, "title", title);
3808                    out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
3809                    out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
3810
3811                    status = HTTP_STATUS_OK;
3812                }
3813                else {
3814                    out = xs_dict_append(out, "error", "cannot create list");
3815                    status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3816                }
3817
3818                *body  = xs_json_dumps(out, 4);
3819                *ctype = "application/json";
3820            }
3821            else
3822                status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3823        }
3824    }
3825    else
3826    if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
3827        if (logged_in) {
3828            xs *l = xs_split(cmd, "/");
3829            const char *op = xs_list_get(l, -1);
3830            const char *id = xs_list_get(l, -2);
3831
3832            if (op && id && xs_is_hex(id)) {
3833                if (strcmp(op, "accounts") == 0) {
3834                    const xs_list *accts = xs_dict_get(args, "account_ids[]");
3835
3836                    if (xs_is_null(accts))
3837                        accts = xs_dict_get(args, "account_ids");
3838
3839                    int c = 0;
3840                    const char *v;
3841
3842                    while (xs_list_next(accts, &v, &c)) {
3843                        list_members(&snac, id, v, 1);
3844                    }
3845
3846                    xs *out = xs_dict_new();
3847                    *body   = xs_json_dumps(out, 4);
3848                    *ctype  = "application/json";
3849                    status  = HTTP_STATUS_OK;
3850                }
3851            }
3852        }
3853    }
3854    else if (strcmp(cmd, "/v1/markers") == 0) { /** **/
3855        xs_str *json = NULL;
3856        if (logged_in) {
3857            const xs_str *home_marker = xs_dict_get(args, "home[last_read_id]");
3858            if (xs_is_null(home_marker)) {
3859                const xs_dict *home = xs_dict_get(args, "home");
3860                if (!xs_is_null(home))
3861                    home_marker = xs_dict_get(home, "last_read_id");
3862            }
3863
3864            const xs_str *notify_marker = xs_dict_get(args, "notifications[last_read_id]");
3865            if (xs_is_null(notify_marker)) {
3866                const xs_dict *notify = xs_dict_get(args, "notifications");
3867                if (!xs_is_null(notify))
3868                    notify_marker = xs_dict_get(notify, "last_read_id");
3869            }
3870            json = xs_json_dumps(markers_set(&snac, home_marker, notify_marker), 4);
3871        }
3872        if (!xs_is_null(json))
3873            *body = json;
3874        else
3875            *body = xs_dup("{}");
3876
3877        *ctype = "application/json";
3878        status = HTTP_STATUS_OK;
3879    }
3880    else
3881    if (xs_startswith(cmd, "/v1/follow_requests")) { /** **/
3882        if (logged_in) {
3883            /* "authorize" or "reject" */
3884            xs *rel = NULL;
3885            xs *l = xs_split(cmd, "/");
3886            const char *md5 = xs_list_get(l, -2);
3887            const char *s_cmd = xs_list_get(l, -1);
3888
3889            if (xs_is_string(md5) && xs_is_string(s_cmd)) {
3890                xs *actor = NULL;
3891
3892                if (valid_status(object_get_by_md5(md5, &actor))) {
3893                    const char *actor_id = xs_dict_get(actor, "id");
3894
3895                    if (strcmp(s_cmd, "authorize") == 0) {
3896                        xs *fwreq = pending_get(&snac, actor_id);
3897
3898                        if (fwreq != NULL) {
3899                            xs *reply = msg_accept(&snac, fwreq, actor_id);
3900
3901                            enqueue_message(&snac, reply);
3902
3903                            if (xs_is_null(xs_dict_get(fwreq, "published"))) {
3904                                xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3905                                fwreq = xs_dict_set(fwreq, "published", date);
3906                            }
3907
3908                            timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
3909
3910                            follower_add(&snac, actor_id);
3911
3912                            pending_del(&snac, actor_id);
3913                            rel = mastoapi_relationship(&snac, md5);
3914                        }
3915                    }
3916                    else
3917                    if (strcmp(s_cmd, "reject") == 0) {
3918                        pending_del(&snac, actor_id);
3919                        rel = mastoapi_relationship(&snac, md5);
3920                    }
3921                }
3922            }
3923
3924            if (rel != NULL) {
3925                *body = xs_json_dumps(rel, 4);
3926                *ctype = "application/json";
3927                status = HTTP_STATUS_OK;
3928            }
3929        }
3930    }
3931    else
3932    if (xs_startswith(cmd, "/v1/tags/")) { /** **/
3933        if (logged_in) {
3934            xs *l = xs_split(cmd, "/");
3935            const char *i_tag = xs_list_get(l, 3);
3936            const char *cmd = xs_list_get(l, 4);
3937
3938            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3939
3940            if (xs_is_string(i_tag) && xs_is_string(cmd)) {
3941                int ok = 0;
3942
3943                xs *tag = xs_fmt("#%s", i_tag);
3944                xs *followed_hashtags = xs_dup(xs_dict_get_def(snac.config,
3945                                "followed_hashtags", xs_stock(XSTYPE_LIST)));
3946
3947                if (strcmp(cmd, "follow") == 0) {
3948                    followed_hashtags = xs_list_append(followed_hashtags, tag);
3949                    ok = 1;
3950                }
3951                else
3952                if (strcmp(cmd, "unfollow") == 0) {
3953                    int off = xs_list_in(followed_hashtags, tag);
3954
3955                    if (off != -1)
3956                        followed_hashtags = xs_list_del(followed_hashtags, off);
3957
3958                    ok = 1;
3959                }
3960
3961                if (ok) {
3962                    /* update */
3963                    xs_dict_set(snac.config, "followed_hashtags", followed_hashtags);
3964                    user_persist(&snac, 0);
3965
3966                    xs *d = xs_dict_new();
3967                    xs *s = xs_fmt("%s?t=%s", srv_baseurl, i_tag);
3968                    d = xs_dict_set(d, "name", i_tag);
3969                    d = xs_dict_set(d, "url", s);
3970                    d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST));
3971
3972                    *body = xs_json_dumps(d, 4);
3973                    *ctype = "application/json";
3974                    status = HTTP_STATUS_OK;
3975                }
3976            }
3977        }
3978    }
3979    else
3980        status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3981
3982    /* user cleanup */
3983    if (logged_in)
3984        user_free(&snac);
3985
3986    srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status));
3987
3988    return status;
3989}
3990
3991
3992int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
3993                          const char *payload, int p_size,
3994                          char **body, int *b_size, char **ctype)
3995{
3996    (void)p_size;
3997    (void)b_size;
3998    (void)ctype;
3999
4000    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
4001        return 0;
4002
4003    int status    = HTTP_STATUS_NOT_FOUND;
4004    xs *args      = NULL;
4005    const char *i_ctype = xs_dict_get(req, "content-type");
4006
4007    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
4008        if (!xs_is_null(payload))
4009            args = xs_json_loads(payload);
4010    }
4011    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
4012    {
4013        // Some apps send form data instead of json so we should cater for those
4014        if (!xs_is_null(payload)) {
4015            args = xs_url_vars(payload);
4016        }
4017    }
4018    else
4019        args = xs_dup(xs_dict_get(req, "p_vars"));
4020
4021    if (args == NULL)
4022        return HTTP_STATUS_BAD_REQUEST;
4023
4024    snac snac = {0};
4025    int logged_in = process_auth_token(&snac, req);
4026
4027    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
4028
4029    if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
4030        // pretend we deleted it, since it doesn't exist anyway
4031        status = HTTP_STATUS_OK;
4032    }
4033    else
4034    if (xs_startswith(cmd, "/v1/lists/")) {
4035        if (logged_in) {
4036            xs *l = xs_split(cmd, "/");
4037            const char *p = xs_list_get(l, -1);
4038
4039            if (p) {
4040                if (strcmp(p, "accounts") == 0) {
4041                    /* delete account from list */
4042                    p = xs_list_get(l, -2);
4043                    const xs_list *accts = xs_dict_get(args, "account_ids[]");
4044
4045                    if (xs_is_null(accts))
4046                        accts = xs_dict_get(args, "account_ids");
4047
4048                    int c = 0;
4049                    const char *v;
4050
4051                    while (xs_list_next(accts, &v, &c)) {
4052                        list_members(&snac, p, v, 2);
4053                    }
4054                }
4055                else {
4056                    /* delete list */
4057                    if (xs_is_hex(p)) {
4058                        list_maint(&snac, p, 2);
4059                    }
4060                }
4061            }
4062
4063            *ctype = "application/json";
4064            status = HTTP_STATUS_OK;
4065        }
4066        else
4067            status = HTTP_STATUS_UNAUTHORIZED;
4068    }
4069    else
4070    if (xs_startswith(cmd, "/v1/statuses/")) {
4071        if (logged_in) {
4072            xs *l = xs_split(cmd, "/");
4073            const char *p = xs_list_get(l, -1);
4074            p = MID_TO_MD5(p);
4075
4076            xs *obj = NULL;
4077            if (valid_status(object_get_by_md5(p, &obj))) {
4078                const char *id = xs_dict_get(obj, "id");
4079
4080                if (xs_is_string(id) && is_msg_mine(&snac, id)) {
4081                    xs *out = mastoapi_status(&snac, obj);
4082
4083                    xs *msg = msg_delete(&snac, id);
4084
4085                    enqueue_message(&snac, msg);
4086
4087                    timeline_del(&snac, id);
4088
4089                    draft_del(&snac, id);
4090
4091                    schedule_del(&snac, id);
4092
4093                    snac_log(&snac, xs_fmt("deleted entry %s", id));
4094
4095                    *body = xs_json_dumps(out, 4);
4096                    *ctype = "application/json";
4097                    status = HTTP_STATUS_OK;
4098                }
4099            }
4100        }
4101        else
4102            status = HTTP_STATUS_UNAUTHORIZED;
4103    }
4104
4105    /* user cleanup */
4106    if (logged_in)
4107        user_free(&snac);
4108
4109    srv_debug(1, xs_fmt("mastoapi_delete_handler %s %d", q_path, status));
4110
4111    return status;
4112}
4113
4114
4115int mastoapi_put_handler(const xs_dict *req, const char *q_path,
4116                          const char *payload, int p_size,
4117                          char **body, int *b_size, char **ctype)
4118{
4119    (void)p_size;
4120    (void)b_size;
4121
4122    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
4123        return 0;
4124
4125    int status    = HTTP_STATUS_NOT_FOUND;
4126    xs *args      = NULL;
4127    const char *i_ctype = xs_dict_get(req, "content-type");
4128
4129    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
4130        if (!xs_is_null(payload))
4131            args = xs_json_loads(payload);
4132    }
4133    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
4134    {
4135        // Some apps send form data instead of json so we should cater for those
4136        if (!xs_is_null(payload)) {
4137            args    = xs_url_vars(payload);
4138        }
4139    }
4140    else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data"))
4141    {
4142        // Handle multipart/form-data by using p_vars (already parsed by httpd)
4143        args = xs_dup(xs_dict_get(req, "p_vars"));
4144    }
4145
4146    /* if args still NULL, try falling back to p_vars or q_vars */
4147    if (args == NULL)
4148        args = xs_dup(xs_dict_get(req, "p_vars"));
4149
4150    if (args == NULL)
4151        args = xs_dup(xs_dict_get(req, "q_vars"));
4152
4153    if (args == NULL) {
4154        srv_debug(1, xs_fmt("mastoapi_put_handler: failed to parse args for %s, content-type: %s",
4155                           q_path, i_ctype ? i_ctype : "(null)"));
4156        return HTTP_STATUS_BAD_REQUEST;
4157    }
4158
4159    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
4160
4161    snac snac = {0};
4162    int logged_in = process_auth_token(&snac, req);
4163
4164    if (xs_startswith(cmd, "/v1/media") || xs_startswith(cmd, "/v2/media")) { /** **/
4165        if (logged_in) {
4166            xs *l = xs_split(cmd, "/");
4167            const char *stid = xs_list_get(l, 3);
4168
4169            if (!xs_is_null(stid)) {
4170                const char *desc = xs_dict_get(args, "description");
4171
4172                /* set the image metadata */
4173                static_put_meta(&snac, stid, desc);
4174
4175                /* prepare a response */
4176                xs *rsp = xs_dict_new();
4177                xs *url = xs_fmt("%s/s/%s", snac.actor, stid);
4178
4179                rsp = xs_dict_append(rsp, "id",          stid);
4180                rsp = xs_dict_append(rsp, "type",        "image");
4181                rsp = xs_dict_append(rsp, "url",         url);
4182                rsp = xs_dict_append(rsp, "preview_url", url);
4183                rsp = xs_dict_append(rsp, "remote_url",  url);
4184                rsp = xs_dict_append(rsp, "description", desc);
4185
4186                *body  = xs_json_dumps(rsp, 4);
4187                *ctype = "application/json";
4188                status = HTTP_STATUS_OK;
4189            }
4190        }
4191        else
4192            status = HTTP_STATUS_UNAUTHORIZED;
4193    }
4194    else
4195    if (xs_startswith(cmd, "/v1/statuses")) {
4196        if (logged_in) {
4197            xs *l = xs_split(cmd, "/");
4198            const char *mid = xs_list_get(l, 3);
4199
4200            if (!xs_is_null(mid)) {
4201                const char *md5 = MID_TO_MD5(mid);
4202                xs *rsp = NULL;
4203                xs *msg = NULL;
4204
4205                if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) {
4206                    const char *content = xs_dict_get(args, "status");
4207                    xs *atls = xs_list_new();
4208                    xs *f_content0 = not_really_markdown(content, &atls, NULL);
4209                    xs *tag = xs_list_new();
4210                    xs *f_content = process_tags(&snac, f_content0, &tag);
4211
4212                    /* replace fields with new content */
4213                    msg = xs_dict_set(msg, "sourceContent", content);
4214                    msg = xs_dict_set(msg, "content", f_content);
4215
4216                    xs *content_map = xs_dup(xs_dict_get(msg, "contentMap"));
4217                    if (xs_is_dict(content_map)) {
4218                        /* get the first pair */
4219                        const char *k, *v;
4220                        int c = 0;
4221
4222                        if (xs_dict_next(content_map, &k, &v, &c)) {
4223                            content_map = xs_dict_set(content_map, k, f_content);
4224                            msg = xs_dict_set(msg, "contentMap", content_map);
4225                        }
4226                    }
4227
4228                    xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
4229                    msg = xs_dict_set(msg, "updated", updated);
4230
4231                    msg = xs_dict_set(msg, "tag", tag);
4232
4233                    /* overwrite object, not updating the indexes */
4234                    const char *id = xs_dict_get(msg, "id");
4235
4236                    object_add_ow(id, msg);
4237
4238                    tag_index(id, msg);
4239
4240                    /* update message */
4241                    xs *c_msg = msg_update(&snac, msg);
4242
4243                    enqueue_message(&snac, c_msg);
4244
4245                    rsp = mastoapi_status(&snac, msg);
4246                }
4247
4248                if (rsp != NULL) {
4249                    *body  = xs_json_dumps(rsp, 4);
4250                    *ctype = "application/json";
4251                    status = HTTP_STATUS_OK;
4252                }
4253            }
4254        }
4255        else
4256            status = HTTP_STATUS_UNAUTHORIZED;
4257    }
4258
4259    /* user cleanup */
4260    if (logged_in)
4261        user_free(&snac);
4262
4263    srv_debug(1, xs_fmt("mastoapi_put_handler %s %d", q_path, status));
4264
4265    return status;
4266}
4267
4268void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac)
4269/* Store header or avatar */
4270{
4271    if (data != NULL) {
4272        if (xs_type(data) == XSTYPE_LIST) {
4273            const char *fn = xs_list_get(data, 0);
4274
4275            if (fn && *fn) {
4276                const char *ext = strrchr(fn, '.');
4277
4278                /* Mona iOS sends always jpg as application/octet-stream with no filename */
4279                if (ext == NULL || strcmp(fn, key) == 0) {
4280                    ext = ".jpg";
4281                }
4282
4283                /* Make sure we have a unique file name, otherwise updated images will not be
4284                 * re-loaded by clients. */
4285                xs *rnd         = random_str();
4286                xs *hash        = xs_md5_hex(rnd, strlen(rnd));
4287                xs *id          = xs_fmt("%s%s", hash, ext);
4288                xs *url         = xs_fmt("%s/s/%s", snac->actor, id);
4289                int fo          = xs_number_get(xs_list_get(data, 1));
4290                int fs          = xs_number_get(xs_list_get(data, 2));
4291
4292                /* store */
4293                static_put(snac, id, payload + fo, fs);
4294
4295                snac->config = xs_dict_set(snac->config, key, url);
4296            }
4297        }
4298    }
4299}
4300
4301int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
4302                          const char *payload, int p_size,
4303                          char **body, int *b_size, char **ctype)
4304/* Handle profile updates */
4305{
4306    (void)p_size;
4307    (void)b_size;
4308
4309    if (!xs_startswith(q_path, "/api/v1/"))
4310        return 0;
4311
4312    int status    = HTTP_STATUS_NOT_FOUND;
4313    xs *args      = NULL;
4314    const char *i_ctype = xs_dict_get(req, "content-type");
4315
4316    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
4317        if (!xs_is_null(payload))
4318            args = xs_json_loads(payload);
4319    }
4320    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
4321    {
4322        // Some apps send form data instead of json so we should cater for those
4323        if (!xs_is_null(payload)) {
4324            args    = xs_url_vars(payload);
4325        }
4326    }
4327    else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data"))
4328    {
4329        // Handle multipart/form-data by using p_vars (already parsed by httpd)
4330        args = xs_dup(xs_dict_get(req, "p_vars"));
4331    }
4332
4333    /* if args still NULL, try falling back to p_vars or q_vars */
4334    if (args == NULL)
4335        args = xs_dup(xs_dict_get(req, "p_vars"));
4336
4337    if (args == NULL)
4338        args = xs_dup(xs_dict_get(req, "q_vars"));
4339
4340    if (args == NULL) {
4341        srv_debug(1, xs_fmt("mastoapi_patch_handler: failed to parse args for %s, content-type: %s",
4342                           q_path, i_ctype ? i_ctype : "(null)"));
4343        return HTTP_STATUS_BAD_REQUEST;
4344    }
4345
4346    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
4347
4348    snac snac = {0};
4349    int logged_in = process_auth_token(&snac, req);
4350
4351    if (xs_startswith(cmd, "/v1/accounts/update_credentials")) { /** **/
4352        /* Update user profile fields */
4353        if (logged_in) {
4354            int c = 0;
4355            const xs_str *k;
4356            const xs_val *v;
4357            const xs_str *field_name = NULL;
4358            xs *new_fields = xs_dict_new();
4359            while (xs_dict_next(args, &k, &v, &c)) {
4360                if (strcmp(k, "display_name") == 0) {
4361                    if (v != NULL)
4362                        snac.config = xs_dict_set(snac.config, "name", v);
4363                }
4364                else
4365                if (strcmp(k, "note") == 0) {
4366                    if (v != NULL)
4367                        snac.config = xs_dict_set(snac.config, "bio", v);
4368                }
4369                else
4370                if (strcmp(k, "bot") == 0) {
4371                    if (v != NULL)
4372                        snac.config = xs_dict_set(snac.config, "bot",
4373                            (strcmp(v, "true") == 0 ||
4374                                strcmp(v, "1") == 0) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
4375                }
4376                else
4377                if (strcmp(k, "source[sensitive]") == 0) {
4378                    if (v != NULL)
4379                        snac.config = xs_dict_set(snac.config, "cw",
4380                            strcmp(v, "true") == 0 ? "open" : "");
4381                }
4382                else
4383                if (strcmp(k, "source[privacy]") == 0) {
4384                    if (v != NULL)
4385                        snac.config = xs_dict_set(snac.config, "private",
4386                            strcmp(v, "private") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
4387                }
4388                else
4389                if (strcmp(k, "header") == 0) {
4390                    persist_image("header", v, payload, &snac);
4391                }
4392                else
4393                if (strcmp(k, "avatar") == 0) {
4394                    persist_image("avatar", v, payload, &snac);
4395                }
4396                else
4397                if (xs_between("fields_attributes", k, "[name]")) {
4398                    field_name = strcmp(v, "") != 0 ? v : NULL;
4399                }
4400                else
4401                if (xs_between("fields_attributes", k, "[value]")) {
4402                    if (field_name != NULL) {
4403                        new_fields = xs_dict_set(new_fields, field_name, v);
4404                        snac.config = xs_dict_set(snac.config, "metadata", new_fields);
4405                    }
4406                }
4407                else
4408                if (strcmp(k, "locked") == 0) {
4409                    snac.config = xs_dict_set(snac.config, "approve_followers",
4410                        xs_stock(strcmp(v, "true") == 0 ? XSTYPE_TRUE : XSTYPE_FALSE));
4411                }
4412                /* we don't have support for the following options, yet
4413                   - discoverable (0/1)
4414                 */
4415            }
4416
4417            /* Persist profile */
4418            if (user_persist(&snac, 1) == 0)
4419                credentials_get(body, ctype, &status, snac);
4420            else
4421                status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
4422        }
4423        else
4424            status = HTTP_STATUS_UNAUTHORIZED;
4425    }
4426
4427    /* user cleanup */
4428    if (logged_in)
4429        user_free(&snac);
4430
4431    srv_debug(1, xs_fmt("mastoapi_patch_handler %s %d", q_path, status));
4432
4433    return status;
4434}
4435
4436
4437void mastoapi_purge(void)
4438{
4439    xs *spec   = xs_fmt("%s/app/" "*.json", srv_basedir);
4440    xs *files  = xs_glob(spec, 1, 0);
4441    xs_list *p = files;
4442    const xs_str *v;
4443
4444    time_t mt = time(NULL) - 3600;
4445
4446    while (xs_list_iter(&p, &v)) {
4447        xs *cid = xs_replace(v, ".json", "");
4448        xs *fn  = _app_fn(cid);
4449
4450        if (mtime(fn) < mt) {
4451            /* get the app */
4452            xs *app = app_get(cid);
4453
4454            if (app) {
4455                /* old apps with no uid are incomplete cruft */
4456                const char *uid = xs_dict_get(app, "uid");
4457
4458                if (xs_is_null(uid) || *uid == '\0') {
4459                    unlink(fn);
4460                    srv_debug(2, xs_fmt("purged %s", fn));
4461                }
4462            }
4463        }
4464    }
4465}
4466
4467
4468#endif /* #ifndef NO_MASTODON_API */