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