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