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