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