2.63 mastoapi.c
   1/* snac - A simple, minimalistic ActivityPub instance */
   2/* copyright (c) 2022 - 2024 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
  19#include "snac.h"
  20
  21#include <sys/time.h>
  22
  23static xs_str *random_str(void)
  24/* just what is says in the tin */
  25{
  26    unsigned int data[4] = {0};
  27
  28    xs_rnd_buf(data, sizeof(data));
  29    return xs_hex_enc((char *)data, sizeof(data));
  30}
  31
  32
  33int app_add(const char *id, const xs_dict *app)
  34/* stores an app */
  35{
  36    if (!xs_is_hex(id))
  37        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
  38
  39    int status = HTTP_STATUS_CREATED;
  40    xs *fn     = xs_fmt("%s/app/", srv_basedir);
  41    FILE *f;
  42
  43    mkdirx(fn);
  44    fn = xs_str_cat(fn, id);
  45    fn = xs_str_cat(fn, ".json");
  46
  47    if ((f = fopen(fn, "w")) != NULL) {
  48        xs_json_dump(app, 4, f);
  49        fclose(f);
  50    }
  51    else
  52        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
  53
  54    return status;
  55}
  56
  57
  58xs_str *_app_fn(const char *id)
  59{
  60    return xs_fmt("%s/app/%s.json", srv_basedir, id);
  61}
  62
  63
  64xs_dict *app_get(const char *id)
  65/* gets an app */
  66{
  67    if (!xs_is_hex(id))
  68        return NULL;
  69
  70    xs *fn       = _app_fn(id);
  71    xs_dict *app = NULL;
  72    FILE *f;
  73
  74    if ((f = fopen(fn, "r")) != NULL) {
  75        app = xs_json_load(f);
  76        fclose(f);
  77
  78    }
  79
  80    return app;
  81}
  82
  83
  84int app_del(const char *id)
  85/* deletes an app */
  86{
  87    if (!xs_is_hex(id))
  88        return -1;
  89
  90    xs *fn = _app_fn(id);
  91
  92    return unlink(fn);
  93}
  94
  95
  96int token_add(const char *id, const xs_dict *token)
  97/* stores a token */
  98{
  99    if (!xs_is_hex(id))
 100        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
 101
 102    int status = HTTP_STATUS_CREATED;
 103    xs *fn     = xs_fmt("%s/token/", srv_basedir);
 104    FILE *f;
 105
 106    mkdirx(fn);
 107    fn = xs_str_cat(fn, id);
 108    fn = xs_str_cat(fn, ".json");
 109
 110    if ((f = fopen(fn, "w")) != NULL) {
 111        xs_json_dump(token, 4, f);
 112        fclose(f);
 113    }
 114    else
 115        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 116
 117    return status;
 118}
 119
 120
 121xs_dict *token_get(const char *id)
 122/* gets a token */
 123{
 124    if (!xs_is_hex(id))
 125        return NULL;
 126
 127    xs *fn         = xs_fmt("%s/token/%s.json", srv_basedir, id);
 128    xs_dict *token = NULL;
 129    FILE *f;
 130
 131    if ((f = fopen(fn, "r")) != NULL) {
 132        token = xs_json_load(f);
 133        fclose(f);
 134
 135        /* 'touch' the file */
 136        utimes(fn, NULL);
 137
 138        /* also 'touch' the app */
 139        const char *app_id = xs_dict_get(token, "client_id");
 140
 141        if (app_id) {
 142            xs *afn = xs_fmt("%s/app/%s.json", srv_basedir, app_id);
 143            utimes(afn, NULL);
 144        }
 145    }
 146
 147    return token;
 148}
 149
 150
 151int token_del(const char *id)
 152/* deletes a token */
 153{
 154    if (!xs_is_hex(id))
 155        return -1;
 156
 157    xs *fn = xs_fmt("%s/token/%s.json", srv_basedir, id);
 158
 159    return unlink(fn);
 160}
 161
 162
 163const char *login_page = ""
 164"<!DOCTYPE html>\n"
 165"<html>\n"
 166"<head>\n"
 167"<title>%s OAuth - Snac2</title>\n"
 168"<meta content=\"width=device-width, initial-scale=1, minimum-scale=1, user-scalable=no\" name=\"viewport\">"
 169"<style>:root {color-scheme: light dark}</style>\n"
 170"</head>\n"
 171"<body><h1>%s OAuth identify</h1>\n"
 172"<div style=\"background-color: red; color: white\">%s</div>\n"
 173"<form method=\"post\" action=\"%s:/" "/%s/%s\">\n"
 174"<p>Login: <input type=\"text\" name=\"login\"></p>\n"
 175"<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
 176"<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
 177"<input type=\"hidden\" name=\"cid\" value=\"%s\">\n"
 178"<input type=\"hidden\" name=\"state\" value=\"%s\">\n"
 179"<input type=\"submit\" value=\"OK\">\n"
 180"</form><p>%s</p></body></html>\n"
 181"";
 182
 183int oauth_get_handler(const xs_dict *req, const char *q_path,
 184                      char **body, int *b_size, char **ctype)
 185{
 186    (void)b_size;
 187
 188    if (!xs_startswith(q_path, "/oauth/"))
 189        return 0;
 190
 191    int status   = HTTP_STATUS_NOT_FOUND;
 192    const xs_dict *msg = xs_dict_get(req, "q_vars");
 193    xs *cmd      = xs_replace_n(q_path, "/oauth", "", 1);
 194
 195    srv_debug(1, xs_fmt("oauth_get_handler %s", q_path));
 196
 197    if (strcmp(cmd, "/authorize") == 0) { /** **/
 198        const char *cid   = xs_dict_get(msg, "client_id");
 199        const char *ruri  = xs_dict_get(msg, "redirect_uri");
 200        const char *rtype = xs_dict_get(msg, "response_type");
 201        const char *state = xs_dict_get(msg, "state");
 202
 203        status = HTTP_STATUS_BAD_REQUEST;
 204
 205        if (cid && ruri && rtype && strcmp(rtype, "code") == 0) {
 206            xs *app = app_get(cid);
 207
 208            if (app != NULL) {
 209                const char *host = xs_dict_get(srv_config, "host");
 210                const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 211
 212                if (xs_is_null(state))
 213                    state = "";
 214
 215                *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-login",
 216                                ruri, cid, state, USER_AGENT);
 217                *ctype = "text/html";
 218                status = HTTP_STATUS_OK;
 219
 220                srv_debug(1, xs_fmt("oauth authorize: generating login page"));
 221            }
 222            else
 223                srv_debug(1, xs_fmt("oauth authorize: bad client_id %s", cid));
 224        }
 225        else
 226            srv_debug(1, xs_fmt("oauth authorize: invalid or unset arguments"));
 227    }
 228    else
 229    if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
 230        const char *host = xs_dict_get(srv_config, "host");
 231        const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 232
 233        *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-get-token",
 234                        "", "", "", USER_AGENT);
 235        *ctype = "text/html";
 236        status = HTTP_STATUS_OK;
 237
 238    }
 239
 240    return status;
 241}
 242
 243
 244int oauth_post_handler(const xs_dict *req, const char *q_path,
 245                       const char *payload, int p_size,
 246                       char **body, int *b_size, char **ctype)
 247{
 248    (void)p_size;
 249    (void)b_size;
 250
 251    if (!xs_startswith(q_path, "/oauth/"))
 252        return 0;
 253
 254    int status   = HTTP_STATUS_NOT_FOUND;
 255
 256    const char *i_ctype = xs_dict_get(req, "content-type");
 257    xs *args      = NULL;
 258
 259    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
 260        if (!xs_is_null(payload))
 261            args = xs_json_loads(payload);
 262    }
 263    else
 264    if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded") && payload) {
 265        args    = xs_url_vars(payload);
 266    }
 267    else
 268        args = xs_dup(xs_dict_get(req, "p_vars"));
 269
 270    if (args == NULL)
 271        return HTTP_STATUS_BAD_REQUEST;
 272
 273    xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
 274
 275    srv_debug(1, xs_fmt("oauth_post_handler %s", q_path));
 276
 277    if (strcmp(cmd, "/x-snac-login") == 0) { /** **/
 278        const char *login  = xs_dict_get(args, "login");
 279        const char *passwd = xs_dict_get(args, "passwd");
 280        const char *redir  = xs_dict_get(args, "redir");
 281        const char *cid    = xs_dict_get(args, "cid");
 282        const char *state  = xs_dict_get(args, "state");
 283        const char *host   = xs_dict_get(srv_config, "host");
 284        const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 285
 286        /* by default, generate another login form with an error */
 287        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-login",
 288                        redir, cid, state, USER_AGENT);
 289        *ctype = "text/html";
 290        status = HTTP_STATUS_OK;
 291
 292        if (login && passwd && redir && cid) {
 293            snac snac;
 294
 295            if (user_open(&snac, login)) {
 296                /* check the login + password */
 297                if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
 298                    /* success! redirect to the desired uri */
 299                    xs *code = random_str();
 300
 301                    xs_free(*body);
 302
 303                    if (strcmp(redir, "urn:ietf:wg:oauth:2.0:oob") == 0) {
 304                        *body = xs_dup(code);
 305                    }
 306                    else {
 307                        if (xs_str_in(redir, "?") != -1)
 308                            *body = xs_fmt("%s&code=%s", redir, code);
 309                        else
 310                            *body = xs_fmt("%s?code=%s", redir, code);
 311
 312                        status = HTTP_STATUS_SEE_OTHER;
 313                    }
 314
 315                    /* if there is a state, add it */
 316                    if (!xs_is_null(state) && *state) {
 317                        *body = xs_str_cat(*body, "&state=");
 318                        *body = xs_str_cat(*body, state);
 319                    }
 320
 321                    srv_log(xs_fmt("oauth x-snac-login: '%s' success, redirect to %s",
 322                                   login, *body));
 323
 324                    /* assign the login to the app */
 325                    xs *app = app_get(cid);
 326
 327                    if (app != NULL) {
 328                        app = xs_dict_set(app, "uid",  login);
 329                        app = xs_dict_set(app, "code", code);
 330                        app_add(cid, app);
 331                    }
 332                    else
 333                        srv_log(xs_fmt("oauth x-snac-login: error getting app %s", cid));
 334                }
 335                else
 336                    srv_debug(1, xs_fmt("oauth x-snac-login: login '%s' incorrect", login));
 337
 338                user_free(&snac);
 339            }
 340            else
 341                srv_debug(1, xs_fmt("oauth x-snac-login: bad user '%s'", login));
 342        }
 343        else
 344            srv_debug(1, xs_fmt("oauth x-snac-login: invalid or unset arguments"));
 345    }
 346    else
 347    if (strcmp(cmd, "/token") == 0) { /** **/
 348        xs *wrk = NULL;
 349        const char *gtype = xs_dict_get(args, "grant_type");
 350        const char *code  = xs_dict_get(args, "code");
 351        const char *cid   = xs_dict_get(args, "client_id");
 352        const char *csec  = xs_dict_get(args, "client_secret");
 353        const char *ruri  = xs_dict_get(args, "redirect_uri");
 354        const char *scope = xs_dict_get(args, "scope");
 355
 356        /* no client_secret? check if it's inside an authorization header
 357           (AndStatus does it this way) */
 358        if (xs_is_null(csec)) {
 359            const char *auhdr = xs_dict_get(req, "authorization");
 360
 361            if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) {
 362                xs *s1 = xs_replace_n(auhdr, "Basic ", "", 1);
 363                int size;
 364                xs *s2 = xs_base64_dec(s1, &size);
 365
 366                if (!xs_is_null(s2)) {
 367                    xs *l1 = xs_split(s2, ":");
 368
 369                    if (xs_list_len(l1) == 2) {
 370                        wrk = xs_dup(xs_list_get(l1, 1));
 371                        csec = wrk;
 372                    }
 373                }
 374            }
 375        }
 376
 377        /* no code? 
 378           I'm not sure of the impacts of this right now, but Subway Tooter does not
 379           provide a code so one must be generated */
 380        if (xs_is_null(code)){
 381            code = random_str();
 382        }
 383        if (gtype && code && cid && csec && ruri) {
 384            xs *app = app_get(cid);
 385
 386            if (app == NULL) {
 387                status = HTTP_STATUS_UNAUTHORIZED;
 388                srv_log(xs_fmt("oauth token: invalid app %s", cid));
 389            }
 390            else
 391            if (strcmp(csec, xs_dict_get(app, "client_secret")) != 0) {
 392                status = HTTP_STATUS_UNAUTHORIZED;
 393                srv_log(xs_fmt("oauth token: invalid client_secret for app %s", cid));
 394            }
 395            else {
 396                xs *rsp   = xs_dict_new();
 397                xs *cat   = xs_number_new(time(NULL));
 398                xs *tokid = random_str();
 399
 400                rsp = xs_dict_append(rsp, "access_token", tokid);
 401                rsp = xs_dict_append(rsp, "token_type",   "Bearer");
 402                rsp = xs_dict_append(rsp, "created_at",   cat);
 403
 404                if (!xs_is_null(scope))
 405                    rsp = xs_dict_append(rsp, "scope", scope);
 406
 407                *body  = xs_json_dumps(rsp, 4);
 408                *ctype = "application/json";
 409                status = HTTP_STATUS_OK;
 410
 411                const char *uid = xs_dict_get(app, "uid");
 412
 413                srv_debug(1, xs_fmt("oauth token: "
 414                                "successful login for %s, new token %s", uid, tokid));
 415
 416                xs *token = xs_dict_new();
 417                token = xs_dict_append(token, "token",         tokid);
 418                token = xs_dict_append(token, "client_id",     cid);
 419                token = xs_dict_append(token, "client_secret", csec);
 420                token = xs_dict_append(token, "uid",           uid);
 421                token = xs_dict_append(token, "code",          code);
 422
 423                token_add(tokid, token);
 424            }
 425        }
 426        else {
 427            srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
 428            status = HTTP_STATUS_BAD_REQUEST;
 429        }
 430    }
 431    else
 432    if (strcmp(cmd, "/revoke") == 0) { /** **/
 433        const char *cid   = xs_dict_get(args, "client_id");
 434        const char *csec  = xs_dict_get(args, "client_secret");
 435        const char *tokid = xs_dict_get(args, "token");
 436
 437        if (cid && csec && tokid) {
 438            xs *token = token_get(tokid);
 439
 440            *body  = xs_str_new("{}");
 441            *ctype = "application/json";
 442
 443            if (token == NULL || strcmp(csec, xs_dict_get(token, "client_secret")) != 0) {
 444                srv_debug(1, xs_fmt("oauth revoke: bad secret for token %s", tokid));
 445                status = HTTP_STATUS_FORBIDDEN;
 446            }
 447            else {
 448                token_del(tokid);
 449                srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
 450                status = HTTP_STATUS_OK;
 451
 452                /* also delete the app, as it serves no purpose from now on */
 453                app_del(cid);
 454            }
 455        }
 456        else {
 457            srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
 458            status = HTTP_STATUS_FORBIDDEN;
 459        }
 460    }
 461    if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
 462        const char *login  = xs_dict_get(args, "login");
 463        const char *passwd = xs_dict_get(args, "passwd");
 464        const char *host   = xs_dict_get(srv_config, "host");
 465        const char *proto  = xs_dict_get_def(srv_config, "protocol", "https");
 466
 467        /* by default, generate another login form with an error */
 468        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-get-token",
 469                        "", "", "", USER_AGENT);
 470        *ctype = "text/html";
 471        status = HTTP_STATUS_OK;
 472
 473        if (login && passwd) {
 474            snac user;
 475
 476            if (user_open(&user, login)) {
 477                /* check the login + password */
 478                if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
 479                    /* success! create a new token */
 480                    xs *tokid = random_str();
 481
 482                    srv_debug(1, xs_fmt("x-snac-new-token: "
 483                                    "successful login for %s, new token %s", login, tokid));
 484
 485                    xs *token = xs_dict_new();
 486                    token = xs_dict_append(token, "token",         tokid);
 487                    token = xs_dict_append(token, "client_id",     "snac-client");
 488                    token = xs_dict_append(token, "client_secret", "");
 489                    token = xs_dict_append(token, "uid",           login);
 490                    token = xs_dict_append(token, "code",          "");
 491
 492                    token_add(tokid, token);
 493
 494                    *ctype = "text/plain";
 495                    xs_free(*body);
 496                    *body = xs_dup(tokid);
 497                }
 498
 499                user_free(&user);
 500            }
 501        }
 502    }
 503
 504    return status;
 505}
 506
 507
 508xs_str *mastoapi_id(const xs_dict *msg)
 509/* returns a somewhat Mastodon-compatible status id */
 510{
 511    const char *id = xs_dict_get(msg, "id");
 512    xs *md5        = xs_md5_hex(id, strlen(id));
 513
 514    return xs_fmt("%10.0f%s", object_ctime_by_md5(md5), md5);
 515}
 516
 517#define MID_TO_MD5(id) (id + 10)
 518
 519
 520xs_dict *mastoapi_account(snac *logged, const xs_dict *actor)
 521/* converts an ActivityPub actor to a Mastodon account */
 522{
 523    const char *id  = xs_dict_get(actor, "id");
 524    const char *pub = xs_dict_get(actor, "published");
 525    const char *proxy = NULL;
 526
 527    if (xs_type(id) != XSTYPE_STRING)
 528        return NULL;
 529
 530    if (logged && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
 531        proxy = logged->actor;
 532
 533    const char *prefu = xs_dict_get(actor, "preferredUsername");
 534
 535    const char *display_name = xs_dict_get(actor, "name");
 536    if (xs_is_null(display_name) || *display_name == '\0')
 537        display_name = prefu;
 538
 539    xs_dict *acct = xs_dict_new();
 540    xs *acct_md5  = xs_md5_hex(id, strlen(id));
 541    acct = xs_dict_append(acct, "id",           acct_md5);
 542    acct = xs_dict_append(acct, "username",     prefu);
 543    acct = xs_dict_append(acct, "display_name", display_name);
 544    acct = xs_dict_append(acct, "discoverable", xs_stock(XSTYPE_TRUE));
 545    acct = xs_dict_append(acct, "group",        xs_stock(XSTYPE_FALSE));
 546    acct = xs_dict_append(acct, "hide_collections", xs_stock(XSTYPE_FALSE));
 547    acct = xs_dict_append(acct, "indexable",    xs_stock(XSTYPE_TRUE));
 548    acct = xs_dict_append(acct, "noindex",      xs_stock(XSTYPE_FALSE));
 549    acct = xs_dict_append(acct, "roles",        xs_stock(XSTYPE_LIST));
 550
 551    {
 552        /* create the acct field as user@host */
 553        xs *l     = xs_split(id, "/");
 554        xs *fquid = xs_fmt("%s@%s", prefu, xs_list_get(l, 2));
 555        acct      = xs_dict_append(acct, "acct", fquid);
 556    }
 557
 558    if (pub)
 559        acct = xs_dict_append(acct, "created_at", pub);
 560    else {
 561        /* unset created_at crashes Tusky, so lie like a mf */
 562        xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
 563        acct = xs_dict_append(acct, "created_at", date);
 564    }
 565
 566    xs *last_status_at = xs_str_utctime(0, "%Y-%m-%d");
 567    acct = xs_dict_append(acct, "last_status_at", last_status_at);
 568
 569    const char *note = xs_dict_get(actor, "summary");
 570    if (xs_is_null(note))
 571        note = "";
 572
 573    if (strcmp(xs_dict_get(actor, "type"), "Service") == 0)
 574        acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_TRUE));
 575    else
 576        acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_FALSE));
 577
 578    acct = xs_dict_append(acct, "note", note);
 579
 580    acct = xs_dict_append(acct, "url", id);
 581    acct = xs_dict_append(acct, "uri", id);
 582
 583    xs *avatar  = NULL;
 584    const xs_dict *av = xs_dict_get(actor, "icon");
 585
 586    if (xs_type(av) == XSTYPE_DICT) {
 587        const char *url = xs_dict_get(av, "url");
 588
 589        if (url != NULL)
 590            avatar = make_url(url, proxy, 1);
 591    }
 592
 593    if (avatar == NULL)
 594        avatar = xs_fmt("%s/susie.png", srv_baseurl);
 595
 596    acct = xs_dict_append(acct, "avatar", avatar);
 597    acct = xs_dict_append(acct, "avatar_static", avatar);
 598
 599    xs *header  = NULL;
 600    const xs_dict *hd = xs_dict_get(actor, "image");
 601
 602    if (xs_type(hd) == XSTYPE_DICT)
 603        header = make_url(xs_dict_get(hd, "url"), proxy, 1);
 604
 605    if (xs_is_null(header))
 606        header = xs_str_new(NULL);
 607
 608    acct = xs_dict_append(acct, "header", header);
 609    acct = xs_dict_append(acct, "header_static", header);
 610
 611    /* emojis */
 612    const xs_list *p;
 613    if (!xs_is_null(p = xs_dict_get(actor, "tag"))) {
 614        xs *eml = xs_list_new();
 615        const xs_dict *v;
 616        int c = 0;
 617
 618        while (xs_list_next(p, &v, &c)) {
 619            const char *type = xs_dict_get(v, "type");
 620
 621            if (!xs_is_null(type) && strcmp(type, "Emoji") == 0) {
 622                const char *name    = xs_dict_get(v, "name");
 623                const xs_dict *icon = xs_dict_get(v, "icon");
 624
 625                if (!xs_is_null(name) && !xs_is_null(icon)) {
 626                    const char *o_url = xs_dict_get(icon, "url");
 627
 628                    if (!xs_is_null(o_url)) {
 629                        xs *url = make_url(o_url, proxy, 1);
 630                        xs *nm = xs_strip_chars_i(xs_dup(name), ":");
 631                        xs *d1 = xs_dict_new();
 632
 633                        d1 = xs_dict_append(d1, "shortcode",         nm);
 634                        d1 = xs_dict_append(d1, "url",               url);
 635                        d1 = xs_dict_append(d1, "static_url",        url);
 636                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
 637
 638                        eml = xs_list_append(eml, d1);
 639                    }
 640                }
 641            }
 642        }
 643
 644        acct = xs_dict_append(acct, "emojis", eml);
 645    }
 646
 647    acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
 648    acct = xs_dict_append(acct, "followers_count", xs_stock(0));
 649    acct = xs_dict_append(acct, "following_count", xs_stock(0));
 650    acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
 651
 652    xs *fields = xs_list_new();
 653    p = xs_dict_get(actor, "attachment");
 654    const xs_dict *v;
 655
 656    /* dict of validated links */
 657    xs_dict *val_links = NULL;
 658    const xs_dict *metadata  = xs_stock(XSTYPE_DICT);
 659    snac user = {0};
 660
 661    if (xs_startswith(id, srv_baseurl)) {
 662        /* if it's a local user, open it and pick its validated links */
 663        if (user_open(&user, prefu)) {
 664            val_links = user.links;
 665            metadata  = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT));
 666        }
 667    }
 668
 669    if (xs_is_null(val_links))
 670        val_links = xs_stock(XSTYPE_DICT);
 671
 672    int c = 0;
 673    while (xs_list_next(p, &v, &c)) {
 674        const char *type  = xs_dict_get(v, "type");
 675        const char *name  = xs_dict_get(v, "name");
 676        const char *value = xs_dict_get(v, "value");
 677
 678        if (!xs_is_null(type) && !xs_is_null(name) &&
 679            !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) {
 680            xs *val_date = NULL;
 681
 682            const char *url = xs_dict_get(metadata, name);
 683
 684            if (!xs_is_null(url) && xs_startswith(url, "https:/" "/")) {
 685                const xs_number *verified_time = xs_dict_get(val_links, url);
 686                if (xs_type(verified_time) == XSTYPE_NUMBER) {
 687                    time_t t = xs_number_get(verified_time);
 688
 689                    if (t > 0)
 690                        val_date = xs_str_utctime(t, ISO_DATE_SPEC);
 691                }
 692            }
 693
 694            xs *d = xs_dict_new();
 695
 696            d = xs_dict_append(d, "name", name);
 697            d = xs_dict_append(d, "value", value);
 698            d = xs_dict_append(d, "verified_at",
 699                xs_type(val_date) == XSTYPE_STRING && *val_date ?
 700                    val_date : xs_stock(XSTYPE_NULL));
 701
 702            fields = xs_list_append(fields, d);
 703        }
 704    }
 705
 706    user_free(&user);
 707
 708    acct = xs_dict_append(acct, "fields", fields);
 709
 710    return acct;
 711}
 712
 713
 714xs_str *mastoapi_date(const char *date)
 715/* converts an ISO 8601 date to whatever format Mastodon uses */
 716{
 717    xs_str *s = xs_crop_i(xs_dup(date), 0, 19);
 718    s = xs_str_cat(s, ".000Z");
 719
 720    return s;
 721}
 722
 723
 724xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg)
 725/* creates a mastoapi Poll object */
 726{
 727    xs_dict *poll = xs_dict_new();
 728    xs *mid       = mastoapi_id(msg);
 729    const xs_list *opts = NULL;
 730    const xs_val *v;
 731    int num_votes = 0;
 732    xs *options = xs_list_new();
 733
 734    poll = xs_dict_append(poll, "id", mid);
 735    const char *date = xs_dict_get(msg, "endTime");
 736    if (date == NULL)
 737        date = xs_dict_get(msg, "closed");
 738    if (date == NULL)
 739        return NULL;
 740
 741    xs *fd = mastoapi_date(date);
 742    poll = xs_dict_append(poll, "expires_at", fd);
 743
 744    date = xs_dict_get(msg, "closed");
 745    time_t t = 0;
 746
 747    if (date != NULL)
 748        t = xs_parse_iso_date(date, 0);
 749
 750    poll = xs_dict_append(poll, "expired",
 751            t < time(NULL) ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE));
 752
 753    if ((opts = xs_dict_get(msg, "oneOf")) != NULL)
 754        poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_FALSE));
 755    else {
 756        opts = xs_dict_get(msg, "anyOf");
 757        poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_TRUE));
 758    }
 759
 760    int c = 0;
 761    while (xs_list_next(opts, &v, &c)) {
 762        const char *title   = xs_dict_get(v, "name");
 763        const char *replies = xs_dict_get(v, "replies");
 764
 765        if (title && replies) {
 766            const char *votes_count = xs_dict_get(replies, "totalItems");
 767
 768            if (xs_type(votes_count) == XSTYPE_NUMBER) {
 769                xs *d = xs_dict_new();
 770                d = xs_dict_append(d, "title",  title);
 771                d = xs_dict_append(d, "votes_count", votes_count);
 772
 773                options = xs_list_append(options, d);
 774                num_votes += xs_number_get(votes_count);
 775            }
 776        }
 777    }
 778
 779    poll = xs_dict_append(poll, "options", options);
 780    xs *vc = xs_number_new(num_votes);
 781    poll = xs_dict_append(poll, "votes_count", vc);
 782
 783    poll = xs_dict_append(poll, "emojis", xs_stock(XSTYPE_LIST));
 784
 785    poll = xs_dict_append(poll, "voted",
 786            (snac && was_question_voted(snac, xs_dict_get(msg, "id"))) ?
 787                xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
 788
 789    return poll;
 790}
 791
 792
 793xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
 794/* converts an ActivityPub note to a Mastodon status */
 795{
 796    xs *actor = NULL;
 797    actor_get_refresh(snac, get_atto(msg), &actor);
 798    const char *proxy = NULL;
 799
 800    /* if the author is not here, discard */
 801    if (actor == NULL)
 802        return NULL;
 803
 804    if (snac && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
 805        proxy = snac->actor;
 806
 807    const char *type = xs_dict_get(msg, "type");
 808    const char *id   = xs_dict_get(msg, "id");
 809
 810    /* fail if it's not a valid actor */
 811    if (xs_is_null(type) || xs_is_null(id))
 812        return NULL;
 813
 814    xs *acct = mastoapi_account(snac, actor);
 815    if (acct == NULL)
 816        return NULL;
 817
 818    xs *idx = NULL;
 819    xs *ixc = NULL;
 820    const char *tmp;
 821    xs *mid  = mastoapi_id(msg);
 822
 823    xs_dict *st = xs_dict_new();
 824
 825    st = xs_dict_append(st, "id",           mid);
 826    st = xs_dict_append(st, "uri",          id);
 827    st = xs_dict_append(st, "url",          id);
 828    st = xs_dict_append(st, "account",      acct);
 829
 830    xs *fd = mastoapi_date(xs_dict_get(msg, "published"));
 831    st = xs_dict_append(st, "created_at", fd);
 832
 833    {
 834        const char *content = xs_dict_get(msg, "content");
 835        const char *name    = xs_dict_get(msg, "name");
 836        xs *s1 = NULL;
 837
 838        if (name && content)
 839            s1 = xs_fmt("%s<br><br>%s", name, content);
 840        else
 841        if (name)
 842            s1 = xs_dup(name);
 843        else
 844        if (content)
 845            s1 = xs_dup(content);
 846        else
 847            s1 = xs_str_new(NULL);
 848
 849        st = xs_dict_append(st, "content", s1);
 850    }
 851
 852    st = xs_dict_append(st, "visibility",
 853        is_msg_public(msg) ? "public" : "private");
 854
 855    tmp = xs_dict_get(msg, "sensitive");
 856    if (xs_is_null(tmp))
 857        tmp = xs_stock(XSTYPE_FALSE);
 858
 859    st = xs_dict_append(st, "sensitive",    tmp);
 860
 861    tmp = xs_dict_get(msg, "summary");
 862    if (xs_is_null(tmp))
 863        tmp = "";
 864
 865    st = xs_dict_append(st, "spoiler_text", tmp);
 866
 867    /* create the list of attachments */
 868    xs *attach = get_attachments(msg);
 869
 870    {
 871        xs_list *p = attach;
 872        const xs_dict *v;
 873
 874        xs *matt = xs_list_new();
 875
 876        while (xs_list_iter(&p, &v)) {
 877            const char *type = xs_dict_get(v, "type");
 878            const char *o_href = xs_dict_get(v, "href");
 879            const char *name = xs_dict_get(v, "name");
 880
 881            if (xs_match(type, "image/*|video/*|Image|Video")) { /* */
 882                xs *matteid = xs_fmt("%s_%d", id, xs_list_len(matt));
 883                xs *href = make_url(o_href, proxy, 1);
 884
 885                xs *d = xs_dict_new();
 886
 887                d = xs_dict_append(d, "id",          matteid);
 888                d = xs_dict_append(d, "url",         href);
 889                d = xs_dict_append(d, "preview_url", href);
 890                d = xs_dict_append(d, "remote_url",  href);
 891                d = xs_dict_append(d, "description", name);
 892
 893                d = xs_dict_append(d, "type", (*type == 'v' || *type == 'V') ? "video" : "image");
 894
 895                matt = xs_list_append(matt, d);
 896            }
 897        }
 898
 899        st = xs_dict_append(st, "media_attachments", matt);
 900    }
 901
 902    {
 903        xs *ml  = xs_list_new();
 904        xs *htl = xs_list_new();
 905        xs *eml = xs_list_new();
 906        const xs_list *tag = xs_dict_get(msg, "tag");
 907        int n = 0;
 908
 909        xs *tag_list = NULL;
 910
 911        if (xs_type(tag) == XSTYPE_DICT) {
 912            tag_list = xs_list_new();
 913            tag_list = xs_list_append(tag_list, tag);
 914        }
 915        else
 916        if (xs_type(tag) == XSTYPE_LIST)
 917            tag_list = xs_dup(tag);
 918        else
 919            tag_list = xs_list_new();
 920
 921        tag = tag_list;
 922        const xs_dict *v;
 923
 924        int c = 0;
 925        while (xs_list_next(tag, &v, &c)) {
 926            const char *type = xs_dict_get(v, "type");
 927
 928            if (xs_is_null(type))
 929                continue;
 930
 931            xs *d1 = xs_dict_new();
 932
 933            if (strcmp(type, "Mention") == 0) {
 934                const char *name = xs_dict_get(v, "name");
 935                const char *href = xs_dict_get(v, "href");
 936
 937                if (!xs_is_null(name) && !xs_is_null(href) &&
 938                    (snac == NULL || strcmp(href, snac->actor) != 0)) {
 939                    xs *nm = xs_strip_chars_i(xs_dup(name), "@");
 940
 941                    xs *id = xs_fmt("%d", n++);
 942                    d1 = xs_dict_append(d1, "id", id);
 943                    d1 = xs_dict_append(d1, "username", nm);
 944                    d1 = xs_dict_append(d1, "acct", nm);
 945                    d1 = xs_dict_append(d1, "url", href);
 946
 947                    ml = xs_list_append(ml, d1);
 948                }
 949            }
 950            else
 951            if (strcmp(type, "Hashtag") == 0) {
 952                const char *name = xs_dict_get(v, "name");
 953                const char *href = xs_dict_get(v, "href");
 954
 955                if (!xs_is_null(name) && !xs_is_null(href)) {
 956                    xs *nm = xs_strip_chars_i(xs_dup(name), "#");
 957
 958                    d1 = xs_dict_append(d1, "name", nm);
 959                    d1 = xs_dict_append(d1, "url", href);
 960
 961                    htl = xs_list_append(htl, d1);
 962                }
 963            }
 964            else
 965            if (strcmp(type, "Emoji") == 0) {
 966                const char *name    = xs_dict_get(v, "name");
 967                const xs_dict *icon = xs_dict_get(v, "icon");
 968
 969                if (!xs_is_null(name) && !xs_is_null(icon)) {
 970                    const char *o_url = xs_dict_get(icon, "url");
 971
 972                    if (!xs_is_null(o_url)) {
 973                        xs *url = make_url(o_url, snac->actor, 1);
 974                        xs *nm = xs_strip_chars_i(xs_dup(name), ":");
 975
 976                        d1 = xs_dict_append(d1, "shortcode", nm);
 977                        d1 = xs_dict_append(d1, "url", url);
 978                        d1 = xs_dict_append(d1, "static_url", url);
 979                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
 980                        d1 = xs_dict_append(d1, "category", "Emojis");
 981
 982                        eml = xs_list_append(eml, d1);
 983                    }
 984                }
 985            }
 986        }
 987
 988        st = xs_dict_append(st, "mentions", ml);
 989        st = xs_dict_append(st, "tags",     htl);
 990        st = xs_dict_append(st, "emojis",   eml);
 991    }
 992
 993    xs_free(idx);
 994    xs_free(ixc);
 995    idx = object_likes(id);
 996    ixc = xs_number_new(xs_list_len(idx));
 997
 998    st = xs_dict_append(st, "favourites_count", ixc);
 999    st = xs_dict_append(st, "favourited",
1000        (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1001
1002    xs_free(idx);
1003    xs_free(ixc);
1004    idx = object_announces(id);
1005    ixc = xs_number_new(xs_list_len(idx));
1006
1007    st = xs_dict_append(st, "reblogs_count", ixc);
1008    st = xs_dict_append(st, "reblogged",
1009        (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1010
1011    /* get the last person who boosted this */
1012    xs *boosted_by_md5 = NULL;
1013    if (xs_list_len(idx))
1014        boosted_by_md5 = xs_dup(xs_list_get(idx, -1));
1015
1016    xs_free(idx);
1017    xs_free(ixc);
1018    idx = object_children(id);
1019    ixc = xs_number_new(xs_list_len(idx));
1020
1021    st = xs_dict_append(st, "replies_count", ixc);
1022
1023    /* default in_reply_to values */
1024    st = xs_dict_append(st, "in_reply_to_id",         xs_stock(XSTYPE_NULL));
1025    st = xs_dict_append(st, "in_reply_to_account_id", xs_stock(XSTYPE_NULL));
1026
1027    tmp = xs_dict_get(msg, "inReplyTo");
1028    if (!xs_is_null(tmp)) {
1029        xs *irto = NULL;
1030
1031        if (valid_status(object_get(tmp, &irto))) {
1032            xs *irt_mid = mastoapi_id(irto);
1033            st = xs_dict_set(st, "in_reply_to_id", irt_mid);
1034
1035            const char *at = NULL;
1036            if (!xs_is_null(at = get_atto(irto))) {
1037                xs *at_md5 = xs_md5_hex(at, strlen(at));
1038                st = xs_dict_set(st, "in_reply_to_account_id", at_md5);
1039            }
1040        }
1041    }
1042
1043    st = xs_dict_append(st, "reblog",   xs_stock(XSTYPE_NULL));
1044    st = xs_dict_append(st, "card",     xs_stock(XSTYPE_NULL));
1045    st = xs_dict_append(st, "language", "en");
1046
1047    st = xs_dict_append(st, "filtered", xs_stock(XSTYPE_LIST));
1048    st = xs_dict_append(st, "muted",    xs_stock(XSTYPE_FALSE));
1049
1050    tmp = xs_dict_get(msg, "sourceContent");
1051    if (xs_is_null(tmp))
1052        tmp = "";
1053
1054    st = xs_dict_append(st, "text", tmp);
1055
1056    tmp = xs_dict_get(msg, "updated");
1057    xs *fd2 = NULL;
1058    if (xs_is_null(tmp))
1059        tmp = xs_stock(XSTYPE_NULL);
1060    else {
1061        fd2 = mastoapi_date(tmp);
1062        tmp = fd2;
1063    }
1064
1065    st = xs_dict_append(st, "edited_at", tmp);
1066
1067    if (strcmp(type, "Question") == 0) {
1068        xs *poll = mastoapi_poll(snac, msg);
1069        st = xs_dict_append(st, "poll", poll);
1070    }
1071    else
1072        st = xs_dict_append(st, "poll", xs_stock(XSTYPE_NULL));
1073
1074    st = xs_dict_append(st, "bookmarked",
1075        (snac && is_bookmarked(snac, id)) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1076
1077    st = xs_dict_append(st, "pinned",
1078        (snac && is_pinned(snac, id)) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1079
1080    /* is it a boost? */
1081    if (!xs_is_null(boosted_by_md5)) {
1082        /* create a new dummy status, using st as the 'reblog' field */
1083        xs_dict *bst = xs_dup(st);
1084        xs *b_actor = NULL;
1085
1086        if (valid_status(object_get_by_md5(boosted_by_md5, &b_actor))) {
1087            xs *b_acct   = mastoapi_account(snac, b_actor);
1088            xs *fake_uri = NULL;
1089
1090            if (snac)
1091                fake_uri = xs_fmt("%s/d/%s/Announce", snac->actor, mid);
1092            else
1093                fake_uri = xs_fmt("%s#%s", srv_baseurl, mid);
1094
1095            bst = xs_dict_set(bst, "uri", fake_uri);
1096            bst = xs_dict_set(bst, "url", fake_uri);
1097            bst = xs_dict_set(bst, "account", b_acct);
1098            bst = xs_dict_set(bst, "content", "");
1099            bst = xs_dict_set(bst, "reblog", st);
1100
1101            xs_free(st);
1102            st = bst;
1103        }
1104    }
1105
1106    return st;
1107}
1108
1109
1110xs_dict *mastoapi_relationship(snac *snac, const char *md5)
1111{
1112    xs_dict *rel = NULL;
1113    xs *actor_o  = NULL;
1114
1115    if (valid_status(object_get_by_md5(md5, &actor_o))) {
1116        rel = xs_dict_new();
1117
1118        const char *actor = xs_dict_get(actor_o, "id");
1119
1120        rel = xs_dict_append(rel, "id",                   md5);
1121        rel = xs_dict_append(rel, "following",
1122            following_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1123
1124        rel = xs_dict_append(rel, "showing_reblogs",      xs_stock(XSTYPE_TRUE));
1125        rel = xs_dict_append(rel, "notifying",            xs_stock(XSTYPE_FALSE));
1126        rel = xs_dict_append(rel, "followed_by",
1127            follower_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1128
1129        rel = xs_dict_append(rel, "blocking",
1130            is_muted(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1131
1132        rel = xs_dict_append(rel, "muting",               xs_stock(XSTYPE_FALSE));
1133        rel = xs_dict_append(rel, "muting_notifications", xs_stock(XSTYPE_FALSE));
1134        rel = xs_dict_append(rel, "requested",            xs_stock(XSTYPE_FALSE));
1135        rel = xs_dict_append(rel, "domain_blocking",      xs_stock(XSTYPE_FALSE));
1136        rel = xs_dict_append(rel, "endorsed",             xs_stock(XSTYPE_FALSE));
1137        rel = xs_dict_append(rel, "note",                 "");
1138    }
1139
1140    return rel;
1141}
1142
1143
1144int process_auth_token(snac *snac, const xs_dict *req)
1145/* processes an authorization token, if there is one */
1146{
1147    int logged_in = 0;
1148    const char *v;
1149
1150    /* if there is an authorization field, try to validate it */
1151    if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
1152        xs *tokid = xs_replace_n(v, "Bearer ", "", 1);
1153        xs *token = token_get(tokid);
1154
1155        if (token != NULL) {
1156            const char *uid = xs_dict_get(token, "uid");
1157
1158            if (!xs_is_null(uid) && user_open(snac, uid)) {
1159                logged_in = 1;
1160
1161                /* this counts as a 'login' */
1162                lastlog_write(snac, "mastoapi");
1163
1164                srv_debug(2, xs_fmt("mastoapi auth: valid token for user '%s'", uid));
1165            }
1166            else
1167                srv_log(xs_fmt("mastoapi auth: corrupted token '%s'", tokid));
1168        }
1169        else
1170            srv_log(xs_fmt("mastoapi auth: invalid token '%s'", tokid));
1171    }
1172
1173    return logged_in;
1174}
1175
1176void credentials_get(char **body, char **ctype, int *status, snac snac)
1177{
1178    xs *acct = xs_dict_new();
1179
1180    acct = xs_dict_append(acct, "id", snac.md5);
1181    acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
1182    acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
1183    acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name"));
1184    acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published"));
1185    acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac.config, "published"));
1186    acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
1187    acct = xs_dict_append(acct, "url", snac.actor);
1188    acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
1189    acct = xs_dict_append(acct, "bot", xs_dict_get(snac.config, "bot"));
1190    acct = xs_dict_append(acct, "emojis", xs_stock(XSTYPE_LIST));
1191
1192    xs *src = xs_json_loads("{\"privacy\":\"public\", \"language\":\"en\","
1193        "\"follow_requests_count\": 0,"
1194        "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
1195    /* some apps take the note from the source object */
1196    src = xs_dict_set(src, "note", xs_dict_get(snac.config, "bio"));
1197    src = xs_dict_set(src, "privacy", xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE ? "private" : "public");
1198
1199    const xs_str *cw = xs_dict_get(snac.config, "cw");
1200    src = xs_dict_set(src, "sensitive",
1201        strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1202
1203    src = xs_dict_set(src, "bot", xs_dict_get(snac.config, "bot"));
1204
1205    xs *avatar = NULL;
1206    const char *av = xs_dict_get(snac.config, "avatar");
1207
1208    if (xs_is_null(av) || *av == '\0')
1209        avatar = xs_fmt("%s/susie.png", srv_baseurl);
1210    else
1211        avatar = xs_dup(av);
1212
1213    acct = xs_dict_append(acct, "avatar", avatar);
1214    acct = xs_dict_append(acct, "avatar_static", avatar);
1215
1216    xs *header = NULL;
1217    const char *hd = xs_dict_get(snac.config, "header");
1218
1219    if (!xs_is_null(hd))
1220        header = xs_dup(hd);
1221    else
1222        header = xs_str_new(NULL);
1223
1224    acct = xs_dict_append(acct, "header", header);
1225    acct = xs_dict_append(acct, "header_static", header);
1226
1227    const xs_dict *metadata = xs_dict_get(snac.config, "metadata");
1228    if (xs_type(metadata) == XSTYPE_DICT) {
1229        xs *fields = xs_list_new();
1230        const xs_str *k;
1231        const xs_str *v;
1232
1233        xs_dict *val_links = snac.links;
1234        if (xs_is_null(val_links))
1235            val_links = xs_stock(XSTYPE_DICT);
1236
1237        int c = 0;
1238        while (xs_dict_next(metadata, &k, &v, &c)) {
1239            xs *val_date = NULL;
1240
1241            const xs_number *verified_time = xs_dict_get(val_links, v);
1242            if (xs_type(verified_time) == XSTYPE_NUMBER) {
1243                time_t t = xs_number_get(verified_time);
1244
1245                if (t > 0)
1246                    val_date = xs_str_utctime(t, ISO_DATE_SPEC);
1247            }
1248
1249            xs *d = xs_dict_new();
1250
1251            d = xs_dict_append(d, "name", k);
1252            d = xs_dict_append(d, "value", v);
1253            d = xs_dict_append(d, "verified_at",
1254                               xs_type(val_date) == XSTYPE_STRING && *val_date ? val_date : xs_stock(XSTYPE_NULL));
1255
1256            fields = xs_list_append(fields, d);
1257        }
1258
1259        acct = xs_dict_set(acct, "fields", fields);
1260        /* some apps take the fields from the source object */
1261        src = xs_dict_set(src, "fields", fields);
1262    }
1263
1264    acct = xs_dict_append(acct, "source", src);
1265    acct = xs_dict_append(acct, "followers_count", xs_stock(0));
1266    acct = xs_dict_append(acct, "following_count", xs_stock(0));
1267    acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
1268
1269    *body = xs_json_dumps(acct, 4);
1270    *ctype = "application/json";
1271    *status = HTTP_STATUS_OK;
1272}
1273
1274
1275xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn)
1276{
1277    xs_list *out = xs_list_new();
1278    FILE *f;
1279    char md5[MD5_HEX_SIZE];
1280
1281    if (dbglevel) {
1282        xs *js = xs_json_dumps(args, 0);
1283        srv_debug(1, xs_fmt("mastoapi_timeline args %s", js));
1284    }
1285
1286    if ((f = fopen(index_fn, "r")) == NULL)
1287        return out;
1288
1289    const char *max_id   = xs_dict_get(args, "max_id");
1290    const char *since_id = xs_dict_get(args, "since_id");
1291    const char *min_id   = xs_dict_get(args, "min_id");
1292    const char *limit_s  = xs_dict_get(args, "limit");
1293    int limit = 0;
1294    int cnt   = 0;
1295
1296    if (!xs_is_null(limit_s))
1297        limit = atoi(limit_s);
1298
1299    if (limit == 0)
1300        limit = 20;
1301
1302    if (index_desc_first(f, md5, 0)) {
1303        do {
1304            xs *msg = NULL;
1305
1306            /* only return entries older that max_id */
1307            if (max_id) {
1308                if (strcmp(md5, MID_TO_MD5(max_id)) == 0)
1309                    max_id = NULL;
1310
1311                continue;
1312            }
1313
1314            /* only returns entries newer than since_id */
1315            if (since_id) {
1316                if (strcmp(md5, MID_TO_MD5(since_id)) == 0)
1317                    break;
1318            }
1319
1320            /* only returns entries newer than min_id */
1321            /* what does really "Return results immediately newer than ID" mean? */
1322            if (min_id) {
1323                if (strcmp(md5, MID_TO_MD5(min_id)) == 0)
1324                    break;
1325            }
1326
1327            /* get the entry */
1328            if (user) {
1329                if (!valid_status(timeline_get_by_md5(user, md5, &msg)))
1330                    continue;
1331            }
1332            else {
1333                if (!valid_status(object_get_by_md5(md5, &msg)))
1334                    continue;
1335            }
1336
1337            /* discard non-Notes */
1338            const char *id   = xs_dict_get(msg, "id");
1339            const char *type = xs_dict_get(msg, "type");
1340            if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
1341                continue;
1342
1343            const char *from = NULL;
1344            if (strcmp(type, "Page") == 0)
1345                from = xs_dict_get(msg, "audience");
1346
1347            if (from == NULL)
1348                from = get_atto(msg);
1349
1350            if (from == NULL)
1351                continue;
1352
1353            if (user) {
1354                /* is this message from a person we don't follow? */
1355                if (strcmp(from, user->actor) && !following_check(user, from)) {
1356                    /* discard if it was not boosted */
1357                    xs *idx = object_announces(id);
1358
1359                    if (xs_list_len(idx) == 0)
1360                        continue;
1361                }
1362
1363                /* discard notes from muted morons */
1364                if (is_muted(user, from))
1365                    continue;
1366
1367                /* discard hidden notes */
1368                if (is_hidden(user, id))
1369                    continue;
1370            }
1371            else {
1372                /* skip non-public messages */
1373                if (!is_msg_public(msg))
1374                    continue;
1375
1376                /* discard messages from private users */
1377                if (is_msg_from_private_user(msg))
1378                    continue;
1379            }
1380
1381            /* if it has a name and it's not a Page or a Video,
1382               it's a poll vote, so discard it */
1383            if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video"))
1384                continue;
1385
1386            /* convert the Note into a Mastodon status */
1387            xs *st = mastoapi_status(user, msg);
1388
1389            if (st != NULL) {
1390                out = xs_list_append(out, st);
1391                cnt++;
1392            }
1393
1394        } while (cnt < limit && index_desc_next(f, md5));
1395    }
1396
1397    int more = index_desc_next(f, md5);
1398
1399    fclose(f);
1400
1401    srv_debug(1, xs_fmt("mastoapi_timeline ret %d%s", cnt, more ? " (+)" : ""));
1402
1403    return out;
1404}
1405
1406
1407int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1408                         char **body, int *b_size, char **ctype)
1409{
1410    (void)b_size;
1411
1412    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
1413        return 0;
1414
1415    int status    = HTTP_STATUS_NOT_FOUND;
1416    const xs_dict *args = xs_dict_get(req, "q_vars");
1417    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
1418
1419    snac snac1 = {0};
1420    int logged_in = process_auth_token(&snac1, req);
1421
1422    if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
1423        if (logged_in) {
1424            credentials_get(body, ctype, &status, snac1);
1425        }
1426        else {
1427            status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login)
1428        }
1429    }
1430    else
1431    if (strcmp(cmd, "/v1/accounts/relationships") == 0) { /** **/
1432        /* find if an account is followed, blocked, etc. */
1433        /* the account to get relationships about is in args "id[]" */
1434
1435        if (logged_in) {
1436            xs *res         = xs_list_new();
1437            const char *md5 = xs_dict_get(args, "id[]");
1438
1439            if (xs_is_null(md5))
1440                md5 = xs_dict_get(args, "id");
1441
1442            if (!xs_is_null(md5)) {
1443                if (xs_type(md5) == XSTYPE_LIST)
1444                    md5 = xs_list_get(md5, 0);
1445
1446                xs *rel = mastoapi_relationship(&snac1, md5);
1447
1448                if (rel != NULL)
1449                    res = xs_list_append(res, rel);
1450            }
1451
1452            *body  = xs_json_dumps(res, 4);
1453            *ctype = "application/json";
1454            status = HTTP_STATUS_OK;
1455        }
1456        else
1457            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
1458    }
1459    else
1460    if (strcmp(cmd, "/v1/accounts/lookup") == 0) { /** **/
1461        /* lookup an account */
1462        const char *acct = xs_dict_get(args, "acct");
1463
1464        if (!xs_is_null(acct)) {
1465            xs *s = xs_strip_chars_i(xs_dup(acct), "@");
1466            xs *l = xs_split_n(s, "@", 1);
1467            const char *uid = xs_list_get(l, 0);
1468            const char *host = xs_list_get(l, 1);
1469
1470            if (uid && (!host || strcmp(host, xs_dict_get(srv_config, "host")) == 0)) {
1471                snac user;
1472
1473                if (user_open(&user, uid)) {
1474                    xs *actor = msg_actor(&user);
1475                    xs *macct = mastoapi_account(NULL, actor);
1476
1477                    *body  = xs_json_dumps(macct, 4);
1478                    *ctype = "application/json";
1479                    status = HTTP_STATUS_OK;
1480
1481                    user_free(&user);
1482                }
1483            }
1484        }
1485    }
1486    else
1487    if (xs_startswith(cmd, "/v1/accounts/")) { /** **/
1488        /* account-related information */
1489        xs *l = xs_split(cmd, "/");
1490        const char *uid = xs_list_get(l, 3);
1491        const char *opt = xs_list_get(l, 4);
1492
1493        if (uid != NULL) {
1494            snac snac2;
1495            xs *out   = NULL;
1496            xs *actor = NULL;
1497
1498            if (logged_in && strcmp(uid, "search") == 0) { /** **/
1499                /* search for accounts starting with q */
1500                const char *aq = xs_dict_get(args, "q");
1501
1502                if (!xs_is_null(aq)) {
1503                    xs *q    = xs_tolower_i(xs_dup(aq));
1504                    out      = xs_list_new();
1505                    xs *wing = following_list(&snac1);
1506                    xs *wers = follower_list(&snac1);
1507                    xs *ulst = user_list();
1508                    xs_list *p;
1509                    const xs_str *v;
1510                    xs_set seen;
1511
1512                    xs_set_init(&seen);
1513
1514                    /* user relations */
1515                    xs_list *lsts[] = { wing, wers, NULL };
1516                    int n;
1517                    for (n = 0; (p = lsts[n]) != NULL; n++) {
1518
1519                        while (xs_list_iter(&p, &v)) {
1520                            /* already seen? skip */
1521                            if (xs_set_add(&seen, v) == 0)
1522                                continue;
1523
1524                            xs *actor = NULL;
1525
1526                            if (valid_status(object_get(v, &actor))) {
1527                                const char *uname = xs_dict_get(actor, "preferredUsername");
1528
1529                                if (!xs_is_null(uname)) {
1530                                    xs *luname = xs_tolower_i(xs_dup(uname));
1531
1532                                    if (xs_startswith(luname, q)) {
1533                                        xs *acct = mastoapi_account(&snac1, actor);
1534
1535                                        out = xs_list_append(out, acct);
1536                                    }
1537                                }
1538                            }
1539                        }
1540                    }
1541
1542                    /* local users */
1543                    p = ulst;
1544                    while (xs_list_iter(&p, &v)) {
1545                        snac user;
1546
1547                        /* skip this same user */
1548                        if (strcmp(v, xs_dict_get(snac1.config, "uid")) == 0)
1549                            continue;
1550
1551                        /* skip if the uid does not start with the query */
1552                        xs *v2 = xs_tolower_i(xs_dup(v));
1553                        if (!xs_startswith(v2, q))
1554                            continue;
1555
1556                        if (user_open(&user, v)) {
1557                            /* if it's not already seen, add it */
1558                            if (xs_set_add(&seen, user.actor) == 1) {
1559                                xs *actor = msg_actor(&user);
1560                                xs *acct  = mastoapi_account(&snac1, actor);
1561
1562                                out = xs_list_append(out, acct);
1563                            }
1564
1565                            user_free(&user);
1566                        }
1567                    }
1568
1569                    xs_set_free(&seen);
1570                }
1571            }
1572            else
1573            /* is it a local user? */
1574            if (user_open(&snac2, uid) || user_open_by_md5(&snac2, uid)) {
1575                if (opt == NULL) {
1576                    /* account information */
1577                    actor = msg_actor(&snac2);
1578                    out   = mastoapi_account(NULL, actor);
1579                }
1580                else
1581                if (strcmp(opt, "statuses") == 0) { /** **/
1582                    /* the public list of posts of a user */
1583                    xs *timeline = timeline_simple_list(&snac2, "public", 0, 256);
1584                    xs_list *p   = timeline;
1585                    const xs_str *v;
1586
1587                    out = xs_list_new();
1588
1589                    while (xs_list_iter(&p, &v)) {
1590                        xs *msg = NULL;
1591
1592                        if (valid_status(timeline_get_by_md5(&snac2, v, &msg))) {
1593                            /* add only posts by the author */
1594                            if (strcmp(xs_dict_get(msg, "type"), "Note") == 0 &&
1595                                xs_startswith(xs_dict_get(msg, "id"), snac2.actor)) {
1596                                xs *st = mastoapi_status(&snac2, msg);
1597
1598                                if (st)
1599                                    out = xs_list_append(out, st);
1600                            }
1601                        }
1602                    }
1603                }
1604                else
1605                if (strcmp(opt, "featured_tags") == 0) {
1606                    /* snac doesn't have features tags, yet? */
1607                    /* implement empty response so apps like Tokodon don't show an error */
1608                    out = xs_list_new();
1609                }
1610                else
1611                if (strcmp(opt, "following") == 0) {
1612                    xs *wing = following_list(&snac1);
1613                    out = xs_list_new();
1614                    int c = 0;
1615                    const char *v;
1616
1617                    while (xs_list_next(wing, &v, &c)) {
1618                        xs *actor = NULL;
1619
1620                        if (valid_status(object_get(v, &actor))) {
1621                            xs *acct = mastoapi_account(NULL, actor);
1622                            out = xs_list_append(out, acct);
1623                        }
1624                    }
1625                }
1626                else
1627                if (strcmp(opt, "followers") == 0) {
1628                    out = xs_list_new();
1629                }
1630
1631                user_free(&snac2);
1632            }
1633            else {
1634                /* try the uid as the md5 of a possibly loaded actor */
1635                if (logged_in && valid_status(object_get_by_md5(uid, &actor))) {
1636                    if (opt == NULL) {
1637                        /* account information */
1638                        out = mastoapi_account(&snac1, actor);
1639                    }
1640                    else
1641                    if (strcmp(opt, "statuses") == 0) {
1642                        /* we don't serve statuses of others; return the empty list */ 
1643                        out = xs_list_new();
1644                    }
1645                    else
1646                    if (strcmp(opt, "featured_tags") == 0) {
1647                        /* snac doesn't have features tags, yet? */
1648                        /* implement empty response so apps like Tokodon don't show an error */
1649                        out = xs_list_new();
1650                    }
1651                }
1652            }
1653
1654            if (out != NULL) {
1655                *body  = xs_json_dumps(out, 4);
1656                *ctype = "application/json";
1657                status = HTTP_STATUS_OK;
1658            }
1659        }
1660    }
1661    else
1662    if (strcmp(cmd, "/v1/timelines/home") == 0) { /** **/
1663        /* the private timeline */
1664        if (logged_in) {
1665            xs *ifn = user_index_fn(&snac1, "private");
1666            xs *out = mastoapi_timeline(&snac1, args, ifn);
1667
1668            *body  = xs_json_dumps(out, 4);
1669            *ctype = "application/json";
1670            status = HTTP_STATUS_OK;
1671
1672            srv_debug(2, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out)));
1673        }
1674        else {
1675            status = HTTP_STATUS_UNAUTHORIZED;
1676        }
1677    }
1678    else
1679    if (strcmp(cmd, "/v1/timelines/public") == 0) { /** **/
1680        /* the instance public timeline (public timelines for all users) */
1681        xs *ifn = instance_index_fn();
1682        xs *out = mastoapi_timeline(NULL, args, ifn);
1683
1684        *body  = xs_json_dumps(out, 4);
1685        *ctype = "application/json";
1686        status = HTTP_STATUS_OK;
1687    }
1688    else
1689    if (xs_startswith(cmd, "/v1/timelines/tag/")) { /** **/
1690        /* get the tag */
1691        xs *l = xs_split(cmd, "/");
1692        const char *tag = xs_list_get(l, -1);
1693
1694        xs *ifn = tag_fn(tag);
1695        xs *out = mastoapi_timeline(NULL, args, ifn);
1696
1697        *body  = xs_json_dumps(out, 4);
1698        *ctype = "application/json";
1699        status = HTTP_STATUS_OK;
1700    }
1701    else
1702    if (xs_startswith(cmd, "/v1/timelines/list/")) { /** **/
1703        /* get the list id */
1704        if (logged_in) {
1705            xs *l = xs_split(cmd, "/");
1706            const char *list = xs_list_get(l, -1);
1707
1708            xs *ifn = list_timeline_fn(&snac1, list);
1709            xs *out = mastoapi_timeline(NULL, args, ifn);
1710
1711            *body  = xs_json_dumps(out, 4);
1712            *ctype = "application/json";
1713            status = HTTP_STATUS_OK;
1714        }
1715        else
1716            status = HTTP_STATUS_MISDIRECTED_REQUEST;
1717    }
1718    else
1719    if (strcmp(cmd, "/v1/conversations") == 0) { /** **/
1720        /* TBD */
1721        *body  = xs_dup("[]");
1722        *ctype = "application/json";
1723        status = HTTP_STATUS_OK;
1724    }
1725    else
1726    if (strcmp(cmd, "/v1/notifications") == 0) { /** **/
1727        if (logged_in) {
1728            xs *l      = notify_list(&snac1, 0, 64);
1729            xs *out    = xs_list_new();
1730            xs_list *p = l;
1731            const xs_dict *v;
1732            const xs_list *excl = xs_dict_get(args, "exclude_types[]");
1733
1734            while (xs_list_iter(&p, &v)) {
1735                xs *noti = notify_get(&snac1, v);
1736
1737                if (noti == NULL)
1738                    continue;
1739
1740                const char *type  = xs_dict_get(noti, "type");
1741                const char *utype = xs_dict_get(noti, "utype");
1742                const char *objid = xs_dict_get(noti, "objid");
1743                xs *actor = NULL;
1744                xs *entry = NULL;
1745
1746                if (!valid_status(actor_get(xs_dict_get(noti, "actor"), &actor)))
1747                    continue;
1748
1749                if (objid != NULL && !valid_status(object_get(objid, &entry)))
1750                    continue;
1751
1752                if (is_hidden(&snac1, objid))
1753                    continue;
1754
1755                /* convert the type */
1756                if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0)
1757                    type = "favourite";
1758                else
1759                if (strcmp(type, "Announce") == 0)
1760                    type = "reblog";
1761                else
1762                if (strcmp(type, "Follow") == 0)
1763                    type = "follow";
1764                else
1765                if (strcmp(type, "Create") == 0)
1766                    type = "mention";
1767                else
1768                if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
1769                    type = "poll";
1770                else
1771                    continue;
1772
1773                /* excluded type? */
1774                if (!xs_is_null(excl) && xs_list_in(excl, type) != -1)
1775                    continue;
1776
1777                xs *mn = xs_dict_new();
1778
1779                mn = xs_dict_append(mn, "type", type);
1780
1781                xs *id = xs_replace(xs_dict_get(noti, "id"), ".", "");
1782                mn = xs_dict_append(mn, "id", id);
1783
1784                mn = xs_dict_append(mn, "created_at", xs_dict_get(noti, "date"));
1785
1786                xs *acct = mastoapi_account(&snac1, actor);
1787                mn = xs_dict_append(mn, "account", acct);
1788
1789                if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) {
1790                    xs *st = mastoapi_status(&snac1, entry);
1791
1792                    if (st)
1793                        mn = xs_dict_append(mn, "status", st);
1794                }
1795
1796                out = xs_list_append(out, mn);
1797            }
1798
1799            *body  = xs_json_dumps(out, 4);
1800            *ctype = "application/json";
1801            status = HTTP_STATUS_OK;
1802        }
1803        else
1804            status = HTTP_STATUS_UNAUTHORIZED;
1805    }
1806    else
1807    if (strcmp(cmd, "/v1/filters") == 0) { /** **/
1808        /* snac will never have filters */
1809        *body  = xs_dup("[]");
1810        *ctype = "application/json";
1811        status = HTTP_STATUS_OK;
1812    }
1813    else
1814    if (strcmp(cmd, "/v2/filters") == 0) { /** **/
1815        /* snac will never have filters 
1816         * but still, without a v2 endpoint a short delay is introduced
1817         * in some apps */
1818        *body  = xs_dup("[]");
1819        *ctype = "application/json";
1820        status = HTTP_STATUS_OK;
1821    }
1822    else
1823    if (strcmp(cmd, "/v1/favourites") == 0) { /** **/
1824        /* snac will never support a list of favourites */
1825        *body  = xs_dup("[]");
1826        *ctype = "application/json";
1827        status = HTTP_STATUS_OK;
1828    }
1829    else
1830    if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/
1831        if (logged_in) {
1832            xs *ifn = bookmark_index_fn(&snac1);
1833            xs *out = mastoapi_timeline(&snac1, args, ifn);
1834
1835            *body  = xs_json_dumps(out, 4);
1836            *ctype = "application/json";
1837            status = HTTP_STATUS_OK;
1838        }
1839        else
1840            status = HTTP_STATUS_UNAUTHORIZED;
1841    }
1842    else
1843    if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
1844        if (logged_in) {
1845            xs *lol = list_maint(&snac1, NULL, 0);
1846            xs *l   = xs_list_new();
1847            int c = 0;
1848            const xs_list *li;
1849
1850            while (xs_list_next(lol, &li, &c)) {
1851                xs *d = xs_dict_new();
1852
1853                d = xs_dict_append(d, "id", xs_list_get(li, 0));
1854                d = xs_dict_append(d, "title", xs_list_get(li, 1));
1855                d = xs_dict_append(d, "replies_policy", "list");
1856                d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
1857
1858                l = xs_list_append(l, d);
1859            }
1860
1861            *body  = xs_json_dumps(l, 4);
1862            *ctype = "application/json";
1863            status = HTTP_STATUS_OK;
1864        }
1865        else
1866            status = HTTP_STATUS_UNAUTHORIZED;
1867    }
1868    else
1869    if (xs_startswith(cmd, "/v1/lists/")) { /** list information **/
1870        if (logged_in) {
1871            xs *l = xs_split(cmd, "/");
1872            const char *p = xs_list_get(l, -1);
1873
1874            if (p) {
1875                if (strcmp(p, "accounts") == 0) {
1876                    p = xs_list_get(l, -2);
1877
1878                    if (p && xs_is_hex(p)) {
1879                        xs *actors = list_content(&snac1, p, NULL, 0);
1880                        xs *out = xs_list_new();
1881                        int c = 0;
1882                        const char *v;
1883
1884                        while (xs_list_next(actors, &v, &c)) {
1885                            xs *actor = NULL;
1886
1887                            if (valid_status(object_get_by_md5(v, &actor))) {
1888                                xs *acct = mastoapi_account(&snac1, actor);
1889                                out = xs_list_append(out, acct);
1890                            }
1891                        }
1892
1893                        *body  = xs_json_dumps(out, 4);
1894                        *ctype = "application/json";
1895                        status = HTTP_STATUS_OK;
1896                    }
1897                }
1898                else
1899                if (xs_is_hex(p)) {
1900                    xs *out = xs_list_new();
1901                    xs *lol = list_maint(&snac1, NULL, 0);
1902                    int c = 0;
1903                    const xs_list *v;
1904
1905                    while (xs_list_next(lol, &v, &c)) {
1906                        const char *id = xs_list_get(v, 0);
1907
1908                        if (id && strcmp(id, p) == 0) {
1909                            xs *d = xs_dict_new();
1910
1911                            d = xs_dict_append(d, "id", p);
1912                            d = xs_dict_append(d, "title", xs_list_get(v, 1));
1913                            d = xs_dict_append(d, "replies_policy", "list");
1914                            d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
1915
1916                            out = xs_dup(d);
1917                            break;
1918                        }
1919                    }
1920
1921                    *body  = xs_json_dumps(out, 4);
1922                    *ctype = "application/json";
1923                    status = HTTP_STATUS_OK;
1924                }
1925            }
1926        }
1927        else
1928            status = HTTP_STATUS_UNAUTHORIZED;
1929    }
1930    else
1931    if (strcmp(cmd, "/v1/scheduled_statuses") == 0) { /** **/
1932        /* snac does not schedule notes */
1933        *body  = xs_dup("[]");
1934        *ctype = "application/json";
1935        status = HTTP_STATUS_OK;
1936    }
1937    else
1938    if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/
1939        /* snac does not support optional follow confirmations */
1940        *body  = xs_dup("[]");
1941        *ctype = "application/json";
1942        status = HTTP_STATUS_OK;
1943    }
1944    else
1945    if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
1946        if (logged_in) {
1947            xs *resp = xs_list_new();
1948            double la = 0.0;
1949            xs *user_la = xs_dup(xs_dict_get(snac1.config, "last_announcement"));
1950            if (user_la != NULL)
1951                la = xs_number_get(user_la);
1952            xs *val_date = xs_str_utctime(la, ISO_DATE_SPEC);
1953
1954            /* contrary to html, we always send the announcement and set the read flag instead */
1955
1956            const t_announcement *annce = announcement(la);
1957            if (annce != NULL && annce->text != NULL) {
1958                xs *an = xs_dict_new();
1959                xs *id = xs_fmt("%d", annce->timestamp);
1960                xs *ct = xs_fmt("<p>%s</p>", annce->text);
1961
1962                an = xs_dict_set(an, "id",           id);
1963                an = xs_dict_set(an, "content",      ct);
1964                an = xs_dict_set(an, "starts_at",    xs_stock(XSTYPE_NULL));
1965                an = xs_dict_set(an, "ends_at",      xs_stock(XSTYPE_NULL));
1966                an = xs_dict_set(an, "all_day",      xs_stock(XSTYPE_TRUE));
1967                an = xs_dict_set(an, "published_at", val_date);
1968                an = xs_dict_set(an, "updated_at",   val_date);
1969                an = xs_dict_set(an, "read",         (annce->timestamp >= la)
1970                    ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE));
1971                an = xs_dict_set(an, "mentions",     xs_stock(XSTYPE_LIST));
1972                an = xs_dict_set(an, "statuses",     xs_stock(XSTYPE_LIST));
1973                an = xs_dict_set(an, "tags",         xs_stock(XSTYPE_LIST));
1974                an = xs_dict_set(an, "emojis",       xs_stock(XSTYPE_LIST));
1975                an = xs_dict_set(an, "reactions",    xs_stock(XSTYPE_LIST));
1976                resp = xs_list_append(resp, an);
1977            }
1978
1979            *body  = xs_json_dumps(resp, 4);
1980            *ctype = "application/json";
1981            status = HTTP_STATUS_OK;
1982        }
1983    }
1984    else
1985    if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
1986        /* are you kidding me? */
1987        *body  = xs_dup("[]");
1988        *ctype = "application/json";
1989        status = HTTP_STATUS_OK;
1990    }
1991    else
1992    if (strcmp(cmd, "/v1/instance") == 0) { /** **/
1993        /* returns an instance object */
1994        xs *ins = xs_dict_new();
1995        const char *host  = xs_dict_get(srv_config, "host");
1996        const char *title = xs_dict_get(srv_config, "title");
1997        const char *sdesc = xs_dict_get(srv_config, "short_description");
1998
1999        ins = xs_dict_append(ins, "uri",         host);
2000        ins = xs_dict_append(ins, "domain",      host);
2001        ins = xs_dict_append(ins, "title",       title && *title ? title : host);
2002        ins = xs_dict_append(ins, "version",     "4.0.0 (not true; really " USER_AGENT ")");
2003        ins = xs_dict_append(ins, "source_url",  WHAT_IS_SNAC_URL);
2004        ins = xs_dict_append(ins, "description", host);
2005
2006        ins = xs_dict_append(ins, "short_description", sdesc && *sdesc ? sdesc : host);
2007
2008        xs *susie = xs_fmt("%s/susie.png", srv_baseurl);
2009        ins = xs_dict_append(ins, "thumbnail", susie);
2010
2011        const char *v = xs_dict_get(srv_config, "admin_email");
2012        if (xs_is_null(v) || *v == '\0')
2013            v = "admin@localhost";
2014
2015        ins = xs_dict_append(ins, "email", v);
2016
2017        ins = xs_dict_append(ins, "rules", xs_stock(XSTYPE_LIST));
2018
2019        xs *l1 = xs_list_append(xs_list_new(), "en");
2020        ins = xs_dict_append(ins, "languages", l1);
2021
2022        xs *wss = xs_fmt("wss:/" "/%s", xs_dict_get(srv_config, "host"));
2023        xs *urls = xs_dict_new();
2024        urls = xs_dict_append(urls, "streaming_api", wss);
2025
2026        ins = xs_dict_append(ins, "urls", urls);
2027
2028        xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock(0));
2029        d2 = xs_dict_append(d2, "status_count", xs_stock(0));
2030        d2 = xs_dict_append(d2, "domain_count", xs_stock(0));
2031        ins = xs_dict_append(ins, "stats", d2);
2032
2033        ins = xs_dict_append(ins, "registrations",     xs_stock(XSTYPE_FALSE));
2034        ins = xs_dict_append(ins, "approval_required", xs_stock(XSTYPE_FALSE));
2035        ins = xs_dict_append(ins, "invites_enabled",   xs_stock(XSTYPE_FALSE));
2036
2037        xs *cfg = xs_dict_new();
2038
2039        {
2040            xs *d11 = xs_json_loads("{\"characters_reserved_per_url\":32,"
2041                "\"max_characters\":100000,\"max_media_attachments\":8}");
2042            cfg = xs_dict_append(cfg, "statuses", d11);
2043
2044            xs *d12 = xs_json_loads("{\"max_featured_tags\":0}");
2045            cfg = xs_dict_append(cfg, "accounts", d12);
2046
2047            xs *d13 = xs_json_loads("{\"image_matrix_limit\":33177600,"
2048                        "\"image_size_limit\":16777216,"
2049                        "\"video_frame_rate_limit\":120,"
2050                        "\"video_matrix_limit\":8294400,"
2051                        "\"video_size_limit\":103809024}"
2052            );
2053
2054            {
2055                /* get the supported mime types from the internal list */
2056                const char **p = xs_mime_types;
2057                xs_set mtypes;
2058                xs_set_init(&mtypes);
2059
2060                while (*p) {
2061                    const char *type = p[1];
2062
2063                    if (xs_startswith(type, "image/") ||
2064                        xs_startswith(type, "video/") ||
2065                        xs_startswith(type, "audio/"))
2066                        xs_set_add(&mtypes, type);
2067
2068                    p += 2;
2069                }
2070
2071                xs *l = xs_set_result(&mtypes);
2072                d13 = xs_dict_append(d13, "supported_mime_types", l);
2073            }
2074
2075            cfg = xs_dict_append(cfg, "media_attachments", d13);
2076
2077            xs *d14 = xs_json_loads("{\"max_characters_per_option\":50,"
2078                "\"max_expiration\":2629746,"
2079                "\"max_options\":8,\"min_expiration\":300}");
2080            cfg = xs_dict_append(cfg, "polls", d14);
2081        }
2082
2083        ins = xs_dict_append(ins, "configuration", cfg);
2084
2085        const char *admin_account = xs_dict_get(srv_config, "admin_account");
2086
2087        if (!xs_is_null(admin_account) && *admin_account) {
2088            snac admin;
2089
2090            if (user_open(&admin, admin_account)) {
2091                xs *actor = msg_actor(&admin);
2092                xs *acct  = mastoapi_account(NULL, actor);
2093
2094                ins = xs_dict_append(ins, "contact_account", acct);
2095
2096                user_free(&admin);
2097            }
2098        }
2099
2100        *body  = xs_json_dumps(ins, 4);
2101        *ctype = "application/json";
2102        status = HTTP_STATUS_OK;
2103    }
2104    else
2105    if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
2106        /* information about a status */
2107        if (logged_in) {
2108            xs *l = xs_split(cmd, "/");
2109            const char *id = xs_list_get(l, 3);
2110            const char *op = xs_list_get(l, 4);
2111
2112            if (!xs_is_null(id)) {
2113                xs *msg = NULL;
2114                xs *out = NULL;
2115
2116                /* skip the 'fake' part of the id */
2117                id = MID_TO_MD5(id);
2118
2119                if (valid_status(object_get_by_md5(id, &msg))) {
2120                    if (op == NULL) {
2121                        if (!is_muted(&snac1, get_atto(msg))) {
2122                            /* return the status itself */
2123                            out = mastoapi_status(&snac1, msg);
2124                        }
2125                    }
2126                    else
2127                    if (strcmp(op, "context") == 0) { /** **/
2128                        /* return ancestors and children */
2129                        xs *anc = xs_list_new();
2130                        xs *des = xs_list_new();
2131                        xs_list *p;
2132                        const xs_str *v;
2133                        char pid[MD5_HEX_SIZE];
2134
2135                        /* build the [grand]parent list, moving up */
2136                        strncpy(pid, id, sizeof(pid));
2137
2138                        while (object_parent(pid, pid)) {
2139                            xs *m2 = NULL;
2140
2141                            if (valid_status(timeline_get_by_md5(&snac1, pid, &m2))) {
2142                                xs *st = mastoapi_status(&snac1, m2);
2143
2144                                if (st)
2145                                    anc = xs_list_insert(anc, 0, st);
2146                            }
2147                            else
2148                                break;
2149                        }
2150
2151                        /* build the children list */
2152                        xs *children = object_children(xs_dict_get(msg, "id"));
2153                        p = children;
2154
2155                        while (xs_list_iter(&p, &v)) {
2156                            xs *m2 = NULL;
2157
2158                            if (valid_status(timeline_get_by_md5(&snac1, v, &m2))) {
2159                                if (xs_is_null(xs_dict_get(m2, "name"))) {
2160                                    xs *st = mastoapi_status(&snac1, m2);
2161
2162                                    if (st)
2163                                        des = xs_list_append(des, st);
2164                                }
2165                            }
2166                        }
2167
2168                        out = xs_dict_new();
2169                        out = xs_dict_append(out, "ancestors",   anc);
2170                        out = xs_dict_append(out, "descendants", des);
2171                    }
2172                    else
2173                    if (strcmp(op, "reblogged_by") == 0 || /** **/
2174                        strcmp(op, "favourited_by") == 0) { /** **/
2175                        /* return the list of people who liked or boosted this */
2176                        out = xs_list_new();
2177
2178                        xs *l = NULL;
2179
2180                        if (op[0] == 'r')
2181                            l = object_announces(xs_dict_get(msg, "id"));
2182                        else
2183                            l = object_likes(xs_dict_get(msg, "id"));
2184
2185                        xs_list *p = l;
2186                        const xs_str *v;
2187
2188                        while (xs_list_iter(&p, &v)) {
2189                            xs *actor2 = NULL;
2190
2191                            if (valid_status(object_get_by_md5(v, &actor2))) {
2192                                xs *acct2 = mastoapi_account(&snac1, actor2);
2193
2194                                out = xs_list_append(out, acct2);
2195                            }
2196                        }
2197                    }
2198                    else
2199                    if (strcmp(op, "source") == 0) { /** **/
2200                        out = xs_dict_new();
2201
2202                        /* get the mastoapi status id */
2203                        out = xs_dict_append(out, "id", xs_list_get(l, 3));
2204
2205                        out = xs_dict_append(out, "text", xs_dict_get(msg, "sourceContent"));
2206                        out = xs_dict_append(out, "spoiler_text", xs_dict_get(msg, "summary"));
2207                    }
2208                }
2209                else
2210                    srv_debug(1, xs_fmt("mastoapi status: bad id %s", id));
2211
2212                if (out != NULL) {
2213                    *body  = xs_json_dumps(out, 4);
2214                    *ctype = "application/json";
2215                    status = HTTP_STATUS_OK;
2216                }
2217            }
2218        }
2219        else
2220            status = HTTP_STATUS_UNAUTHORIZED;
2221    }
2222    else
2223    if (strcmp(cmd, "/v1/preferences") == 0) { /** **/
2224        *body  = xs_dup("{}");
2225        *ctype = "application/json";
2226        status = HTTP_STATUS_OK;
2227    }
2228    else
2229    if (strcmp(cmd, "/v1/markers") == 0) { /** **/
2230        *body  = xs_dup("{}");
2231        *ctype = "application/json";
2232        status = HTTP_STATUS_OK;
2233    }
2234    else
2235    if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
2236        *body  = xs_dup("[]");
2237        *ctype = "application/json";
2238        status = HTTP_STATUS_OK;
2239    }
2240    else
2241    if (strcmp(cmd, "/v1/trends/tags") == 0) { /** **/
2242        *body  = xs_dup("[]");
2243        *ctype = "application/json";
2244        status = HTTP_STATUS_OK;
2245    }
2246    else
2247    if (strcmp(cmd, "/v1/trends/statuses") == 0) { /** **/
2248        *body  = xs_dup("[]");
2249        *ctype = "application/json";
2250        status = HTTP_STATUS_OK;
2251    }
2252    else
2253    if (strcmp(cmd, "/v2/search") == 0) { /** **/
2254        if (logged_in) {
2255            const char *q      = xs_dict_get(args, "q");
2256            const char *type   = xs_dict_get(args, "type");
2257            const char *offset = xs_dict_get(args, "offset");
2258
2259            xs *acl = xs_list_new();
2260            xs *stl = xs_list_new();
2261            xs *htl = xs_list_new();
2262            xs *res = xs_dict_new();
2263
2264            if (xs_is_null(offset) || strcmp(offset, "0") == 0) {
2265                /* reply something only for offset 0; otherwise,
2266                   apps like Tusky keep asking again and again */
2267
2268                if (!xs_is_null(q)) {
2269                    if (xs_is_null(type) || strcmp(type, "accounts") == 0) {
2270                        /* do a webfinger query */
2271                        char *actor = NULL;
2272                        char *user  = NULL;
2273
2274                        if (valid_status(webfinger_request(q, &actor, &user)) && actor) {
2275                            xs *actor_o = NULL;
2276
2277                            if (valid_status(actor_request(&snac1, actor, &actor_o))) {
2278                                xs *acct = mastoapi_account(NULL, actor_o);
2279
2280                                acl = xs_list_append(acl, acct);
2281
2282                                if (!object_here(actor))
2283                                    object_add(actor, actor_o);
2284                            }
2285                        }
2286                    }
2287
2288                    if (xs_is_null(type) || strcmp(type, "hashtags") == 0) {
2289                        /* search this tag */
2290                        xs *tl = tag_search((char *)q, 0, 1);
2291
2292                        if (xs_list_len(tl)) {
2293                            xs *d = xs_dict_new();
2294
2295                            d = xs_dict_append(d, "name", q);
2296                            xs *url = xs_fmt("%s?t=%s", srv_baseurl, q);
2297                            d = xs_dict_append(d, "url", url);
2298                            d = xs_dict_append(d, "history", xs_stock(XSTYPE_LIST));
2299
2300                            htl = xs_list_append(htl, d);
2301                        }
2302                    }
2303
2304                    if (xs_is_null(type) || strcmp(type, "statuses") == 0) {
2305                        int to = 0;
2306                        int cnt = 40;
2307                        xs *tl = content_search(&snac1, q, 1, 0, cnt, 0, &to);
2308                        int c = 0;
2309                        const char *v;
2310
2311                        while (xs_list_next(tl, &v, &c) && --cnt) {
2312                            xs *post = NULL;
2313
2314                            if (!valid_status(timeline_get_by_md5(&snac1, v, &post)))
2315                                continue;
2316
2317                            xs *s = mastoapi_status(&snac1, post);
2318
2319                            if (!xs_is_null(s))
2320                                stl = xs_list_append(stl, s);
2321                        }
2322                    }
2323                }
2324            }
2325
2326            res = xs_dict_append(res, "accounts", acl);
2327            res = xs_dict_append(res, "statuses", stl);
2328            res = xs_dict_append(res, "hashtags", htl);
2329
2330            *body  = xs_json_dumps(res, 4);
2331            *ctype = "application/json";
2332            status = HTTP_STATUS_OK;
2333        }
2334        else
2335            status = HTTP_STATUS_UNAUTHORIZED;
2336    }
2337
2338    /* user cleanup */
2339    if (logged_in)
2340        user_free(&snac1);
2341
2342    srv_debug(1, xs_fmt("mastoapi_get_handler %s %d", q_path, status));
2343
2344    return status;
2345}
2346
2347
2348int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2349                          const char *payload, int p_size,
2350                          char **body, int *b_size, char **ctype)
2351{
2352    (void)p_size;
2353    (void)b_size;
2354
2355    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2356        return 0;
2357
2358    int status    = HTTP_STATUS_NOT_FOUND;
2359    xs *args      = NULL;
2360    const char *i_ctype = xs_dict_get(req, "content-type");
2361
2362    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
2363        if (!xs_is_null(payload))
2364            args = xs_json_loads(payload);
2365    }
2366    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
2367    {
2368        // Some apps send form data instead of json so we should cater for those
2369        if (!xs_is_null(payload)) {
2370            args    = xs_url_vars(payload);
2371        }
2372    }
2373    else
2374        args = xs_dup(xs_dict_get(req, "p_vars"));
2375
2376    if (args == NULL)
2377        return HTTP_STATUS_BAD_REQUEST;
2378
2379    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
2380
2381    snac snac = {0};
2382    int logged_in = process_auth_token(&snac, req);
2383
2384    if (strcmp(cmd, "/v1/apps") == 0) { /** **/
2385        const char *name  = xs_dict_get(args, "client_name");
2386        const char *ruri  = xs_dict_get(args, "redirect_uris");
2387        const char *scope = xs_dict_get(args, "scope");
2388
2389        /* Ice Cubes sends these values as query parameters, so try these */
2390        if (name == NULL && ruri == NULL && scope == NULL) {
2391            args = xs_dup(xs_dict_get(req, "q_vars"));
2392            name  = xs_dict_get(args, "client_name");
2393            ruri  = xs_dict_get(args, "redirect_uris");
2394            scope = xs_dict_get(args, "scope");
2395        }
2396
2397        if (xs_type(ruri) == XSTYPE_LIST)
2398            ruri = xs_dict_get(ruri, 0);
2399
2400        if (name && ruri) {
2401            xs *app  = xs_dict_new();
2402            xs *id   = xs_replace_i(tid(0), ".", "");
2403            xs *csec = random_str();
2404            xs *vkey = random_str();
2405            xs *cid  = NULL;
2406
2407            /* pick a non-existent random cid */
2408            for (;;) {
2409                cid = random_str();
2410                xs *p_app = app_get(cid);
2411
2412                if (p_app == NULL)
2413                    break;
2414
2415                xs_free(cid);
2416            }
2417
2418            app = xs_dict_append(app, "name",          name);
2419            app = xs_dict_append(app, "redirect_uri",  ruri);
2420            app = xs_dict_append(app, "client_id",     cid);
2421            app = xs_dict_append(app, "client_secret", csec);
2422            app = xs_dict_append(app, "vapid_key",     vkey);
2423            app = xs_dict_append(app, "id",            id);
2424
2425            *body  = xs_json_dumps(app, 4);
2426            *ctype = "application/json";
2427            status = HTTP_STATUS_OK;
2428
2429            app = xs_dict_append(app, "code", "");
2430
2431            if (scope)
2432                app = xs_dict_append(app, "scope", scope);
2433
2434            app_add(cid, app);
2435
2436            srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
2437        }
2438    }
2439    else
2440    if (strcmp(cmd, "/v1/statuses") == 0) { /** **/
2441        if (logged_in) {
2442            /* post a new Note */
2443            const char *content    = xs_dict_get(args, "status");
2444            const char *mid        = xs_dict_get(args, "in_reply_to_id");
2445            const char *visibility = xs_dict_get(args, "visibility");
2446            const char *summary    = xs_dict_get(args, "spoiler_text");
2447            const char *media_ids  = xs_dict_get(args, "media_ids");
2448
2449            if (xs_is_null(media_ids))
2450                media_ids = xs_dict_get(args, "media_ids[]");
2451
2452            if (xs_is_null(media_ids))
2453                media_ids = xs_dict_get(args, "media_ids");
2454
2455            if (xs_is_null(visibility))
2456                visibility = "public";
2457
2458            xs *attach_list = xs_list_new();
2459            xs *irt         = NULL;
2460
2461            /* is it a reply? */
2462            if (mid != NULL) {
2463                xs *r_msg = NULL;
2464                const char *md5 = MID_TO_MD5(mid);
2465
2466                if (valid_status(object_get_by_md5(md5, &r_msg)))
2467                    irt = xs_dup(xs_dict_get(r_msg, "id"));
2468            }
2469
2470            /* does it have attachments? */
2471            if (!xs_is_null(media_ids)) {
2472                xs *mi = NULL;
2473
2474                if (xs_type(media_ids) == XSTYPE_LIST)
2475                    mi = xs_dup(media_ids);
2476                else {
2477                    mi = xs_list_new();
2478                    mi = xs_list_append(mi, media_ids);
2479                }
2480
2481                xs_list *p = mi;
2482                const xs_str *v;
2483
2484                while (xs_list_iter(&p, &v)) {
2485                    xs *l    = xs_list_new();
2486                    xs *url  = xs_fmt("%s/s/%s", snac.actor, v);
2487                    xs *desc = static_get_meta(&snac, v);
2488
2489                    l = xs_list_append(l, url);
2490                    l = xs_list_append(l, desc);
2491
2492                    attach_list = xs_list_append(attach_list, l);
2493                }
2494            }
2495
2496            /* prepare the message */
2497            xs *msg = msg_note(&snac, content, NULL, irt, attach_list,
2498                        strcmp(visibility, "public") == 0 ? 0 : 1);
2499
2500            if (!xs_is_null(summary) && *summary) {
2501                msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
2502                msg = xs_dict_set(msg, "summary",   summary);
2503            }
2504
2505            /* store */
2506            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
2507
2508            /* 'Create' message */
2509            xs *c_msg = msg_create(&snac, msg);
2510            enqueue_message(&snac, c_msg);
2511
2512            timeline_touch(&snac);
2513
2514            /* convert to a mastodon status as a response code */
2515            xs *st = mastoapi_status(&snac, msg);
2516
2517            *body  = xs_json_dumps(st, 4);
2518            *ctype = "application/json";
2519            status = HTTP_STATUS_OK;
2520        }
2521        else
2522            status = HTTP_STATUS_UNAUTHORIZED;
2523    }
2524    else
2525    if (xs_startswith(cmd, "/v1/statuses")) { /** **/
2526        if (logged_in) {
2527            /* operations on a status */
2528            xs *l = xs_split(cmd, "/");
2529            const char *mid = xs_list_get(l, 3);
2530            const char *op  = xs_list_get(l, 4);
2531
2532            if (!xs_is_null(mid)) {
2533                xs *msg = NULL;
2534                xs *out = NULL;
2535
2536                /* skip the 'fake' part of the id */
2537                mid = MID_TO_MD5(mid);
2538
2539                if (valid_status(timeline_get_by_md5(&snac, mid, &msg))) {
2540                    const char *id = xs_dict_get(msg, "id");
2541
2542                    if (op == NULL) {
2543                        /* no operation (?) */
2544                    }
2545                    else
2546                    if (strcmp(op, "favourite") == 0) { /** **/
2547                        xs *n_msg = msg_admiration(&snac, id, "Like");
2548
2549                        if (n_msg != NULL) {
2550                            enqueue_message(&snac, n_msg);
2551                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1);
2552
2553                            out = mastoapi_status(&snac, msg);
2554                        }
2555                    }
2556                    else
2557                    if (strcmp(op, "unfavourite") == 0) { /** **/
2558                        xs *n_msg = msg_repulsion(&snac, id, "Like");
2559
2560                        if (n_msg != NULL) {
2561                            enqueue_message(&snac, n_msg);
2562
2563                            out = mastoapi_status(&snac, msg);
2564                        }
2565                    }
2566                    else
2567                    if (strcmp(op, "reblog") == 0) { /** **/
2568                        xs *n_msg = msg_admiration(&snac, id, "Announce");
2569
2570                        if (n_msg != NULL) {
2571                            enqueue_message(&snac, n_msg);
2572                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0);
2573
2574                            out = mastoapi_status(&snac, msg);
2575                        }
2576                    }
2577                    else
2578                    if (strcmp(op, "unreblog") == 0) { /** **/
2579                        xs *n_msg = msg_repulsion(&snac, id, "Announce");
2580
2581                        if (n_msg != NULL) {
2582                            enqueue_message(&snac, n_msg);
2583
2584                            out = mastoapi_status(&snac, msg);
2585                        }
2586                    }
2587                    else
2588                    if (strcmp(op, "bookmark") == 0) { /** **/
2589                        /* bookmark this message */
2590                        if (bookmark(&snac, id) == 0)
2591                            out = mastoapi_status(&snac, msg);
2592                        else
2593                            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
2594                    }
2595                    else
2596                    if (strcmp(op, "unbookmark") == 0) { /** **/
2597                        /* unbookmark this message */
2598                        unbookmark(&snac, id);
2599                        out = mastoapi_status(&snac, msg);
2600                    }
2601                    else
2602                    if (strcmp(op, "pin") == 0) { /** **/
2603                        /* pin this message */
2604                        if (pin(&snac, id) == 0)
2605                            out = mastoapi_status(&snac, msg);
2606                        else
2607                            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
2608                    }
2609                    else
2610                    if (strcmp(op, "unpin") == 0) { /** **/
2611                        /* unpin this message */
2612                        unpin(&snac, id);
2613                        out = mastoapi_status(&snac, msg);
2614                    }
2615                    else
2616                    if (strcmp(op, "mute") == 0) { /** **/
2617                        /* Mastodon's mute is snac's hide */
2618                    }
2619                    else
2620                    if (strcmp(op, "unmute") == 0) { /** **/
2621                        /* Mastodon's unmute is snac's unhide */
2622                    }
2623                }
2624
2625                if (out != NULL) {
2626                    *body  = xs_json_dumps(out, 4);
2627                    *ctype = "application/json";
2628                    status = HTTP_STATUS_OK;
2629                }
2630            }
2631        }
2632        else
2633            status = HTTP_STATUS_UNAUTHORIZED;
2634    }
2635    else
2636    if (strcmp(cmd, "/v1/notifications/clear") == 0) { /** **/
2637        if (logged_in) {
2638            notify_clear(&snac);
2639            timeline_touch(&snac);
2640
2641            *body  = xs_dup("{}");
2642            *ctype = "application/json";
2643            status = HTTP_STATUS_OK;
2644        }
2645        else
2646            status = HTTP_STATUS_UNAUTHORIZED;
2647    }
2648    else
2649    if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
2650        /* I don't know what I'm doing */
2651        if (logged_in) {
2652            const char *v;
2653
2654            xs *wpush = xs_dict_new();
2655
2656            wpush = xs_dict_append(wpush, "id", "1");
2657
2658            v = xs_dict_get(args, "data");
2659            v = xs_dict_get(v, "alerts");
2660            wpush = xs_dict_append(wpush, "alerts", v);
2661
2662            v = xs_dict_get(args, "subscription");
2663            v = xs_dict_get(v, "endpoint");
2664            wpush = xs_dict_append(wpush, "endpoint", v);
2665
2666            xs *server_key = random_str();
2667            wpush = xs_dict_append(wpush, "server_key", server_key);
2668
2669            *body  = xs_json_dumps(wpush, 4);
2670            *ctype = "application/json";
2671            status = HTTP_STATUS_OK;
2672        }
2673        else
2674            status = HTTP_STATUS_UNAUTHORIZED;
2675    }
2676    else
2677    if (strcmp(cmd, "/v1/media") == 0 || strcmp(cmd, "/v2/media") == 0) { /** **/
2678        if (logged_in) {
2679            const xs_list *file = xs_dict_get(args, "file");
2680            const char *desc    = xs_dict_get(args, "description");
2681
2682            if (xs_is_null(desc))
2683                desc = "";
2684
2685            status = HTTP_STATUS_BAD_REQUEST;
2686
2687            if (xs_type(file) == XSTYPE_LIST) {
2688                const char *fn = xs_list_get(file, 0);
2689
2690                if (*fn != '\0') {
2691                    char *ext = strrchr(fn, '.');
2692                    xs *hash  = xs_md5_hex(fn, strlen(fn));
2693                    xs *id    = xs_fmt("%s%s", hash, ext);
2694                    xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
2695                    int fo    = xs_number_get(xs_list_get(file, 1));
2696                    int fs    = xs_number_get(xs_list_get(file, 2));
2697
2698                    /* store */
2699                    static_put(&snac, id, payload + fo, fs);
2700                    static_put_meta(&snac, id, desc);
2701
2702                    /* prepare a response */
2703                    xs *rsp = xs_dict_new();
2704
2705                    rsp = xs_dict_append(rsp, "id",          id);
2706                    rsp = xs_dict_append(rsp, "type",        "image");
2707                    rsp = xs_dict_append(rsp, "url",         url);
2708                    rsp = xs_dict_append(rsp, "preview_url", url);
2709                    rsp = xs_dict_append(rsp, "remote_url",  url);
2710                    rsp = xs_dict_append(rsp, "description", desc);
2711
2712                    *body  = xs_json_dumps(rsp, 4);
2713                    *ctype = "application/json";
2714                    status = HTTP_STATUS_OK;
2715                }
2716            }
2717        }
2718        else
2719            status = HTTP_STATUS_UNAUTHORIZED;
2720    }
2721    else
2722    if (xs_startswith(cmd, "/v1/accounts")) { /** **/
2723        if (logged_in) {
2724            /* account-related information */
2725            xs *l = xs_split(cmd, "/");
2726            const char *md5 = xs_list_get(l, 3);
2727            const char *opt = xs_list_get(l, 4);
2728            xs *rsp = NULL;
2729
2730            if (!xs_is_null(md5) && *md5) {
2731                xs *actor_o = NULL;
2732
2733                if (xs_is_null(opt)) {
2734                    /* ? */
2735                }
2736                else
2737                if (strcmp(opt, "follow") == 0) { /** **/
2738                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
2739                        const char *actor = xs_dict_get(actor_o, "id");
2740
2741                        xs *msg = msg_follow(&snac, actor);
2742
2743                        if (msg != NULL) {
2744                            /* reload the actor from the message, in may be different */
2745                            actor = xs_dict_get(msg, "object");
2746
2747                            following_add(&snac, actor, msg);
2748
2749                            enqueue_output_by_actor(&snac, msg, actor, 0);
2750
2751                            rsp = mastoapi_relationship(&snac, md5);
2752                        }
2753                    }
2754                }
2755                else
2756                if (strcmp(opt, "unfollow") == 0) { /** **/
2757                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
2758                        const char *actor = xs_dict_get(actor_o, "id");
2759
2760                        /* get the following object */
2761                        xs *object = NULL;
2762
2763                        if (valid_status(following_get(&snac, actor, &object))) {
2764                            xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
2765
2766                            following_del(&snac, actor);
2767
2768                            enqueue_output_by_actor(&snac, msg, actor, 0);
2769
2770                            rsp = mastoapi_relationship(&snac, md5);
2771                        }
2772                    }
2773                }
2774                else
2775                if (strcmp(opt, "block") == 0) { /** **/
2776                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
2777                        const char *actor = xs_dict_get(actor_o, "id");
2778
2779                        mute(&snac, actor);
2780
2781                        rsp = mastoapi_relationship(&snac, md5);
2782                    }
2783                }
2784                else
2785                if (strcmp(opt, "unblock") == 0) { /** **/
2786                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
2787                        const char *actor = xs_dict_get(actor_o, "id");
2788
2789                        unmute(&snac, actor);
2790
2791                        rsp = mastoapi_relationship(&snac, md5);
2792                    }
2793                }
2794            }
2795
2796            if (rsp != NULL) {
2797                *body  = xs_json_dumps(rsp, 4);
2798                *ctype = "application/json";
2799                status = HTTP_STATUS_OK;
2800            }
2801        }
2802        else
2803            status = HTTP_STATUS_UNAUTHORIZED;
2804    }
2805    else
2806    if (xs_startswith(cmd, "/v1/polls")) { /** **/
2807        if (logged_in) {
2808            /* operations on a status */
2809            xs *l = xs_split(cmd, "/");
2810            const char *mid = xs_list_get(l, 3);
2811            const char *op  = xs_list_get(l, 4);
2812
2813            if (!xs_is_null(mid)) {
2814                xs *msg = NULL;
2815                xs *out = NULL;
2816
2817                /* skip the 'fake' part of the id */
2818                mid = MID_TO_MD5(mid);
2819
2820                if (valid_status(timeline_get_by_md5(&snac, mid, &msg))) {
2821                    const char *id   = xs_dict_get(msg, "id");
2822                    const char *atto = get_atto(msg);
2823
2824                    const xs_list *opts = xs_dict_get(msg, "oneOf");
2825                    if (opts == NULL)
2826                        opts = xs_dict_get(msg, "anyOf");
2827
2828                    if (op == NULL) {
2829                    }
2830                    else
2831                    if (strcmp(op, "votes") == 0) {
2832                        const xs_list *choices = xs_dict_get(args, "choices[]");
2833
2834                        if (xs_is_null(choices))
2835                            choices = xs_dict_get(args, "choices");
2836
2837                        if (xs_type(choices) == XSTYPE_LIST) {
2838                            const xs_str *v;
2839
2840                            int c = 0;
2841                            while (xs_list_next(choices, &v, &c)) {
2842                                int io           = atoi(v);
2843                                const xs_dict *o = xs_list_get(opts, io);
2844
2845                                if (o) {
2846                                    const char *name = xs_dict_get(o, "name");
2847
2848                                    xs *msg = msg_note(&snac, "", atto, (char *)id, NULL, 1);
2849                                    msg = xs_dict_append(msg, "name", name);
2850
2851                                    xs *c_msg = msg_create(&snac, msg);
2852                                    enqueue_message(&snac, c_msg);
2853                                    timeline_add(&snac, xs_dict_get(msg, "id"), msg);
2854                                }
2855                            }
2856
2857                            out = mastoapi_poll(&snac, msg);
2858                        }
2859                    }
2860                }
2861
2862                if (out != NULL) {
2863                    *body  = xs_json_dumps(out, 4);
2864                    *ctype = "application/json";
2865                    status = HTTP_STATUS_OK;
2866                }
2867            }
2868        }
2869        else
2870            status = HTTP_STATUS_UNAUTHORIZED;
2871    }
2872    else
2873    if (strcmp(cmd, "/v1/lists") == 0) {
2874        if (logged_in) {
2875            const char *title = xs_dict_get(args, "title");
2876
2877            if (xs_type(title) == XSTYPE_STRING) {
2878                /* add the list */
2879                xs *out = xs_dict_new();
2880                xs *lid = list_maint(&snac, title, 1);
2881
2882                if (!xs_is_null(lid)) {
2883                    out = xs_dict_append(out, "id", lid);
2884                    out = xs_dict_append(out, "title", title);
2885                    out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
2886                    out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
2887
2888                    status = HTTP_STATUS_OK;
2889                }
2890                else {
2891                    out = xs_dict_append(out, "error", "cannot create list");
2892                    status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
2893                }
2894
2895                *body  = xs_json_dumps(out, 4);
2896                *ctype = "application/json";
2897            }
2898            else
2899                status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
2900        }
2901    }
2902    if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
2903        if (logged_in) {
2904            xs *l = xs_split(cmd, "/");
2905            const char *op = xs_list_get(l, -1);
2906            const char *id = xs_list_get(l, -2);
2907
2908            if (op && id && xs_is_hex(id)) {
2909                if (strcmp(op, "accounts") == 0) {
2910                    const xs_list *accts = xs_dict_get(args, "account_ids[]");
2911
2912                    if (xs_is_null(accts))
2913                        accts = xs_dict_get(args, "account_ids");
2914
2915                    int c = 0;
2916                    const char *v;
2917
2918                    while (xs_list_next(accts, &v, &c)) {
2919                        list_content(&snac, id, v, 1);
2920                    }
2921
2922                    xs *out = xs_dict_new();
2923                    *body   = xs_json_dumps(out, 4);
2924                    *ctype  = "application/json";
2925                    status  = HTTP_STATUS_OK;
2926                }
2927            }
2928        }
2929        else
2930            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
2931    }
2932
2933    /* user cleanup */
2934    if (logged_in)
2935        user_free(&snac);
2936
2937    srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status));
2938
2939    return status;
2940}
2941
2942
2943int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
2944                          const char *payload, int p_size,
2945                          char **body, int *b_size, char **ctype)
2946{
2947    (void)p_size;
2948    (void)body;
2949    (void)b_size;
2950    (void)ctype;
2951
2952    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2953        return 0;
2954
2955    int status    = HTTP_STATUS_NOT_FOUND;
2956    xs *args      = NULL;
2957    const char *i_ctype = xs_dict_get(req, "content-type");
2958
2959    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
2960        if (!xs_is_null(payload))
2961            args = xs_json_loads(payload);
2962    }
2963    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
2964    {
2965        // Some apps send form data instead of json so we should cater for those
2966        if (!xs_is_null(payload)) {
2967            args = xs_url_vars(payload);
2968        }
2969    }
2970    else
2971        args = xs_dup(xs_dict_get(req, "p_vars"));
2972
2973    if (args == NULL)
2974        return HTTP_STATUS_BAD_REQUEST;
2975
2976    snac snac = {0};
2977    int logged_in = process_auth_token(&snac, req);
2978
2979    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
2980
2981    if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
2982        // pretend we deleted it, since it doesn't exist anyway
2983        status = HTTP_STATUS_OK;
2984    }
2985    else
2986    if (xs_startswith(cmd, "/v1/lists/")) {
2987        if (logged_in) {
2988            xs *l = xs_split(cmd, "/");
2989            const char *p = xs_list_get(l, -1);
2990
2991            if (p) {
2992                if (strcmp(p, "accounts") == 0) {
2993                    /* delete account from list */
2994                    p = xs_list_get(l, -2);
2995                    const xs_list *accts = xs_dict_get(args, "account_ids[]");
2996
2997                    if (xs_is_null(accts))
2998                        accts = xs_dict_get(args, "account_ids");
2999
3000                    int c = 0;
3001                    const char *v;
3002
3003                    while (xs_list_next(accts, &v, &c)) {
3004                        list_content(&snac, p, v, 2);
3005                    }
3006                }
3007                else {
3008                    /* delete list */
3009                    if (xs_is_hex(p)) {
3010                        list_maint(&snac, p, 2);
3011                    }
3012                }
3013            }
3014
3015            *ctype = "application/json";
3016            status = HTTP_STATUS_OK;
3017        }
3018        else
3019            status = HTTP_STATUS_UNAUTHORIZED;
3020    }
3021
3022    /* user cleanup */
3023    if (logged_in)
3024        user_free(&snac);
3025
3026    srv_debug(1, xs_fmt("mastoapi_delete_handler %s %d", q_path, status));
3027
3028    return status;
3029}
3030
3031
3032int mastoapi_put_handler(const xs_dict *req, const char *q_path,
3033                          const char *payload, int p_size,
3034                          char **body, int *b_size, char **ctype)
3035{
3036    (void)p_size;
3037    (void)b_size;
3038
3039    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
3040        return 0;
3041
3042    int status    = HTTP_STATUS_NOT_FOUND;
3043    xs *args      = NULL;
3044    const char *i_ctype = xs_dict_get(req, "content-type");
3045
3046    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
3047        if (!xs_is_null(payload))
3048            args = xs_json_loads(payload);
3049    }
3050    else
3051        args = xs_dup(xs_dict_get(req, "p_vars"));
3052
3053    if (args == NULL)
3054        return HTTP_STATUS_BAD_REQUEST;
3055
3056    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
3057
3058    snac snac = {0};
3059    int logged_in = process_auth_token(&snac, req);
3060
3061    if (xs_startswith(cmd, "/v1/media") || xs_startswith(cmd, "/v2/media")) { /** **/
3062        if (logged_in) {
3063            xs *l = xs_split(cmd, "/");
3064            const char *stid = xs_list_get(l, 3);
3065
3066            if (!xs_is_null(stid)) {
3067                const char *desc = xs_dict_get(args, "description");
3068
3069                /* set the image metadata */
3070                static_put_meta(&snac, stid, desc);
3071
3072                /* prepare a response */
3073                xs *rsp = xs_dict_new();
3074                xs *url = xs_fmt("%s/s/%s", snac.actor, stid);
3075
3076                rsp = xs_dict_append(rsp, "id",          stid);
3077                rsp = xs_dict_append(rsp, "type",        "image");
3078                rsp = xs_dict_append(rsp, "url",         url);
3079                rsp = xs_dict_append(rsp, "preview_url", url);
3080                rsp = xs_dict_append(rsp, "remote_url",  url);
3081                rsp = xs_dict_append(rsp, "description", desc);
3082
3083                *body  = xs_json_dumps(rsp, 4);
3084                *ctype = "application/json";
3085                status = HTTP_STATUS_OK;
3086            }
3087        }
3088        else
3089            status = HTTP_STATUS_UNAUTHORIZED;
3090    }
3091    else
3092    if (xs_startswith(cmd, "/v1/statuses")) {
3093        if (logged_in) {
3094            xs *l = xs_split(cmd, "/");
3095            const char *mid = xs_list_get(l, 3);
3096
3097            if (!xs_is_null(mid)) {
3098                const char *md5 = MID_TO_MD5(mid);
3099                xs *rsp = NULL;
3100                xs *msg = NULL;
3101
3102                if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) {
3103                    const char *content = xs_dict_get(args, "status");
3104                    xs *atls = xs_list_new();
3105                    xs *f_content = not_really_markdown(content, &atls, NULL);
3106
3107                    /* replace fields with new content */
3108                    msg = xs_dict_set(msg, "sourceContent", content);
3109                    msg = xs_dict_set(msg, "content", f_content);
3110
3111                    xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
3112                    msg = xs_dict_set(msg, "updated", updated);
3113
3114                    /* overwrite object, not updating the indexes */
3115                    object_add_ow(xs_dict_get(msg, "id"), msg);
3116
3117                    /* update message */
3118                    xs *c_msg = msg_update(&snac, msg);
3119
3120                    enqueue_message(&snac, c_msg);
3121
3122                    rsp = mastoapi_status(&snac, msg);
3123                }
3124
3125                if (rsp != NULL) {
3126                    *body  = xs_json_dumps(rsp, 4);
3127                    *ctype = "application/json";
3128                    status = HTTP_STATUS_OK;
3129                }
3130            }
3131        }
3132        else
3133            status = HTTP_STATUS_UNAUTHORIZED;
3134    }
3135
3136    /* user cleanup */
3137    if (logged_in)
3138        user_free(&snac);
3139
3140    srv_debug(1, xs_fmt("mastoapi_put_handler %s %d", q_path, status));
3141
3142    return status;
3143}
3144
3145void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac)
3146/* Store header or avatar */
3147{
3148    if (data != NULL) {
3149        if (xs_type(data) == XSTYPE_LIST) {
3150            const char *fn = xs_list_get(data, 0);
3151
3152            if (fn && *fn) {
3153                const char *ext = strrchr(fn, '.');
3154
3155                /* Mona iOS sends always jpg as application/octet-stream with no filename */
3156                if (ext == NULL || strcmp(fn, key) == 0) {
3157                    ext = ".jpg";
3158                }
3159
3160                /* Make sure we have a unique file name, otherwise updated images will not be
3161                 * re-loaded by clients. */
3162                xs *rnd         = random_str();
3163                xs *hash        = xs_md5_hex(rnd, strlen(rnd));
3164                xs *id          = xs_fmt("%s%s", hash, ext);
3165                xs *url         = xs_fmt("%s/s/%s", snac->actor, id);
3166                int fo          = xs_number_get(xs_list_get(data, 1));
3167                int fs          = xs_number_get(xs_list_get(data, 2));
3168
3169                /* store */
3170                static_put(snac, id, payload + fo, fs);
3171
3172                snac->config = xs_dict_set(snac->config, key, url);
3173            }
3174        }
3175    }
3176}
3177
3178int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
3179                          const char *payload, int p_size,
3180                          char **body, int *b_size, char **ctype)
3181/* Handle profile updates */
3182{
3183    (void)p_size;
3184    (void)b_size;
3185
3186    if (!xs_startswith(q_path, "/api/v1/"))
3187        return 0;
3188
3189    int status    = HTTP_STATUS_NOT_FOUND;
3190    xs *args      = NULL;
3191    const char *i_ctype = xs_dict_get(req, "content-type");
3192
3193    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
3194        if (!xs_is_null(payload))
3195            args = xs_json_loads(payload);
3196    }
3197    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
3198    {
3199        // Some apps send form data instead of json so we should cater for those
3200        if (!xs_is_null(payload)) {
3201            args    = xs_url_vars(payload);
3202        }
3203    }
3204    else
3205        args = xs_dup(xs_dict_get(req, "p_vars"));
3206
3207    if (args == NULL)
3208        return HTTP_STATUS_BAD_REQUEST;
3209
3210    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
3211
3212    snac snac = {0};
3213    int logged_in = process_auth_token(&snac, req);
3214
3215    if (xs_startswith(cmd, "/v1/accounts/update_credentials")) {
3216        /* Update user profile fields */
3217        if (logged_in) {
3218            int c = 0;
3219            const xs_str *k;
3220            const xs_val *v;
3221            const xs_str *field_name = NULL;
3222            xs *new_fields = xs_dict_new();
3223            while (xs_dict_next(args, &k, &v, &c)) {
3224                if (strcmp(k, "display_name") == 0) {
3225                    if (v != NULL)
3226                        snac.config = xs_dict_set(snac.config, "name", v);
3227                }
3228                else
3229                if (strcmp(k, "note") == 0) {
3230                    if (v != NULL)
3231                        snac.config = xs_dict_set(snac.config, "bio", v);
3232                }
3233                else
3234                if (strcmp(k, "bot") == 0) {
3235                    if (v != NULL)
3236                        snac.config = xs_dict_set(snac.config, "bot",
3237                            (strcmp(v, "true") == 0 ||
3238                                strcmp(v, "1") == 0) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
3239                }
3240                else
3241                if (strcmp(k, "source[sensitive]") == 0) {
3242                    if (v != NULL)
3243                        snac.config = xs_dict_set(snac.config, "cw",
3244                            strcmp(v, "true") == 0 ? "open" : "");
3245                }
3246                else
3247                if (strcmp(k, "source[privacy]") == 0) {
3248                    if (v != NULL)
3249                        snac.config = xs_dict_set(snac.config, "private",
3250                            strcmp(v, "private") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
3251                }
3252                else
3253                if (strcmp(k, "header") == 0) {
3254                    persist_image("header", v, payload, &snac);
3255                }
3256                else
3257                if (strcmp(k, "avatar") == 0) {
3258                    persist_image("avatar", v, payload, &snac);
3259                }
3260                else
3261                if (xs_between("fields_attributes", k, "[name]")) {
3262                    field_name = strcmp(v, "") != 0 ? v : NULL;
3263                }
3264                else
3265                if (xs_between("fields_attributes", k, "[value]")) {
3266                    if (field_name != NULL) {
3267                        new_fields = xs_dict_set(new_fields, field_name, v);
3268                        snac.config = xs_dict_set(snac.config, "metadata", new_fields);
3269                    }
3270                }
3271                /* we don't have support for the following options, yet
3272                   - discoverable (0/1)
3273                   - locked (0/1)
3274                 */
3275            }
3276
3277            /* Persist profile */
3278            if (user_persist(&snac, 1) == 0)
3279                credentials_get(body, ctype, &status, snac);
3280            else
3281                status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
3282        }
3283        else
3284            status = HTTP_STATUS_UNAUTHORIZED;
3285    }
3286
3287    /* user cleanup */
3288    if (logged_in)
3289        user_free(&snac);
3290
3291    srv_debug(1, xs_fmt("mastoapi_patch_handler %s %d", q_path, status));
3292
3293    return status;
3294}
3295
3296
3297void mastoapi_purge(void)
3298{
3299    xs *spec   = xs_fmt("%s/app/" "*.json", srv_basedir);
3300    xs *files  = xs_glob(spec, 1, 0);
3301    xs_list *p = files;
3302    const xs_str *v;
3303
3304    time_t mt = time(NULL) - 3600;
3305
3306    while (xs_list_iter(&p, &v)) {
3307        xs *cid = xs_replace(v, ".json", "");
3308        xs *fn  = _app_fn(cid);
3309
3310        if (mtime(fn) < mt) {
3311            /* get the app */
3312            xs *app = app_get(cid);
3313
3314            if (app) {
3315                /* old apps with no uid are incomplete cruft */
3316                const char *uid = xs_dict_get(app, "uid");
3317
3318                if (xs_is_null(uid) || *uid == '\0') {
3319                    unlink(fn);
3320                    srv_debug(2, xs_fmt("purged %s", fn));
3321                }
3322            }
3323        }
3324    }
3325}
3326
3327
3328#endif /* #ifndef NO_MASTODON_API */