2.53 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 *timeline = timeline_simple_list(&snac1, "private", 0, 2048);
1471
1472            xs *out      = xs_list_new();
1473            xs_list *p   = timeline;
1474            xs_str *v;
1475
1476            while (xs_list_iter(&p, &v) && cnt < limit) {
1477                xs *msg = NULL;
1478
1479                /* only return entries older that max_id */
1480                if (max_id) {
1481                    if (strcmp(v, MID_TO_MD5(max_id)) == 0)
1482                        max_id = NULL;
1483
1484                    continue;
1485                }
1486
1487                /* only returns entries newer than since_id */
1488                if (since_id) {
1489                    if (strcmp(v, MID_TO_MD5(since_id)) == 0)
1490                        break;
1491                }
1492
1493                /* only returns entries newer than min_id */
1494                /* what does really "Return results immediately newer than ID" mean? */
1495                if (min_id) {
1496                    if (strcmp(v, MID_TO_MD5(min_id)) == 0)
1497                        break;
1498                }
1499
1500                /* get the entry */
1501                if (!valid_status(timeline_get_by_md5(&snac1, v, &msg)))
1502                    continue;
1503
1504                /* discard non-Notes */
1505                const char *id   = xs_dict_get(msg, "id");
1506                const char *type = xs_dict_get(msg, "type");
1507                if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
1508                    continue;
1509
1510                const char *from = NULL;
1511                if (strcmp(type, "Page") == 0)
1512                    from = xs_dict_get(msg, "audience");
1513
1514                if (from == NULL)
1515                    from = get_atto(msg);
1516
1517                if (from == NULL)
1518                    continue;
1519
1520                /* is this message from a person we don't follow? */
1521                if (strcmp(from, snac1.actor) && !following_check(&snac1, from)) {
1522                    /* discard if it was not boosted */
1523                    xs *idx = object_announces(id);
1524
1525                    if (xs_list_len(idx) == 0)
1526                        continue;
1527                }
1528
1529                /* discard notes from muted morons */
1530                if (is_muted(&snac1, from))
1531                    continue;
1532
1533                /* discard hidden notes */
1534                if (is_hidden(&snac1, id))
1535                    continue;
1536
1537                /* if it has a name and it's not a Page or a Video,
1538                   it's a poll vote, so discard it */
1539                if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video"))
1540                    continue;
1541
1542                /* convert the Note into a Mastodon status */
1543                xs *st = mastoapi_status(&snac1, msg);
1544
1545                if (st != NULL)
1546                    out = xs_list_append(out, st);
1547
1548                cnt++;
1549            }
1550
1551            *body  = xs_json_dumps(out, 4);
1552            *ctype = "application/json";
1553            status = 200;
1554
1555            srv_debug(2, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out)));
1556        }
1557        else {
1558            status = 401; // unauthorized
1559        }
1560    }
1561    else
1562    if (strcmp(cmd, "/v1/timelines/public") == 0) { /** **/
1563        /* the instance public timeline (public timelines for all users) */
1564
1565        const char *limit_s = xs_dict_get(args, "limit");
1566        int limit = 0;
1567        int cnt   = 0;
1568
1569        if (!xs_is_null(limit_s))
1570            limit = atoi(limit_s);
1571
1572        if (limit == 0)
1573            limit = 20;
1574
1575        xs *timeline = timeline_instance_list(0, limit);
1576        xs *out      = xs_list_new();
1577        xs_list *p   = timeline;
1578        xs_str *md5;
1579
1580        snac *user = NULL;
1581        if (logged_in)
1582            user = &snac1;
1583
1584        while (xs_list_iter(&p, &md5) && cnt < limit) {
1585            xs *msg = NULL;
1586
1587            /* get the entry */
1588            if (!valid_status(object_get_by_md5(md5, &msg)))
1589                continue;
1590
1591            /* discard non-Notes */
1592            const char *type = xs_dict_get(msg, "type");
1593            if (strcmp(type, "Note") != 0 && strcmp(type, "Question") != 0)
1594                continue;
1595
1596            /* discard messages from private users */
1597            if (is_msg_from_private_user(msg))
1598                continue;
1599
1600            /* convert the Note into a Mastodon status */
1601            xs *st = mastoapi_status(user, msg);
1602
1603            if (st != NULL) {
1604                out = xs_list_append(out, st);
1605                cnt++;
1606            }
1607        }
1608
1609        *body  = xs_json_dumps(out, 4);
1610        *ctype = "application/json";
1611        status = 200;
1612    }
1613    else
1614    if (xs_startswith(cmd, "/v1/timelines/tag/")) { /** **/
1615        const char *limit_s = xs_dict_get(args, "limit");
1616        int limit = 0;
1617        int cnt   = 0;
1618
1619        if (!xs_is_null(limit_s))
1620            limit = atoi(limit_s);
1621
1622        if (limit == 0)
1623            limit = 20;
1624
1625        /* get the tag */
1626        xs *l = xs_split(cmd, "/");
1627        char *tag = xs_list_get(l, -1);
1628
1629        xs *timeline = tag_search(tag, 0, limit);
1630        xs *out      = xs_list_new();
1631        xs_list *p   = timeline;
1632        xs_str *md5;
1633
1634        while (xs_list_iter(&p, &md5) && cnt < limit) {
1635            xs *msg = NULL;
1636
1637            /* get the entry */
1638            if (!valid_status(object_get_by_md5(md5, &msg)))
1639                continue;
1640
1641            /* skip non-public messages */
1642            if (!is_msg_public(msg))
1643                continue;
1644
1645            /* discard messages from private users */
1646            if (is_msg_from_private_user(msg))
1647                continue;
1648
1649            /* convert the Note into a Mastodon status */
1650            xs *st = mastoapi_status(NULL, msg);
1651
1652            if (st != NULL) {
1653                out = xs_list_append(out, st);
1654                cnt++;
1655            }
1656        }
1657
1658        *body  = xs_json_dumps(out, 4);
1659        *ctype = "application/json";
1660        status = 200;
1661    }
1662    else
1663    if (xs_startswith(cmd, "/v1/timelines/list/")) { /** **/
1664        /* get the list id */
1665        if (logged_in) {
1666            xs *l = xs_split(cmd, "/");
1667            char *list = xs_list_get(l, -1);
1668
1669            xs *timeline = list_timeline(&snac1, list, 0, 2048);
1670            xs *out      = xs_list_new();
1671            int c = 0;
1672            char *md5;
1673
1674            while (xs_list_next(timeline, &md5, &c)) {
1675                xs *msg = NULL;
1676
1677                /* get the entry */
1678                if (!valid_status(timeline_get_by_md5(&snac1, md5, &msg)))
1679                    continue;
1680
1681                /* discard non-Notes */
1682                const char *id   = xs_dict_get(msg, "id");
1683                const char *type = xs_dict_get(msg, "type");
1684                if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
1685                    continue;
1686
1687                const char *from = NULL;
1688                if (strcmp(type, "Page") == 0)
1689                    from = xs_dict_get(msg, "audience");
1690
1691                if (from == NULL)
1692                    from = get_atto(msg);
1693
1694                if (from == NULL)
1695                    continue;
1696
1697                /* is this message from a person we don't follow? */
1698                if (strcmp(from, snac1.actor) && !following_check(&snac1, from)) {
1699                    /* discard if it was not boosted */
1700                    xs *idx = object_announces(id);
1701
1702                    if (xs_list_len(idx) == 0)
1703                        continue;
1704                }
1705
1706                /* discard notes from muted morons */
1707                if (is_muted(&snac1, from))
1708                    continue;
1709
1710                /* discard hidden notes */
1711                if (is_hidden(&snac1, id))
1712                    continue;
1713
1714                /* if it has a name and it's not a Page or a Video,
1715                   it's a poll vote, so discard it */
1716                if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video"))
1717                    continue;
1718
1719                /* convert the Note into a Mastodon status */
1720                xs *st = mastoapi_status(&snac1, msg);
1721
1722                if (st != NULL)
1723                    out = xs_list_append(out, st);
1724            }
1725
1726            *body  = xs_json_dumps(out, 4);
1727            *ctype = "application/json";
1728            status = 200;
1729        }
1730        else
1731            status = 421;
1732    }
1733    else
1734    if (strcmp(cmd, "/v1/conversations") == 0) { /** **/
1735        /* TBD */
1736        *body  = xs_dup("[]");
1737        *ctype = "application/json";
1738        status = 200;
1739    }
1740    else
1741    if (strcmp(cmd, "/v1/notifications") == 0) { /** **/
1742        if (logged_in) {
1743            xs *l      = notify_list(&snac1, 0, 64);
1744            xs *out    = xs_list_new();
1745            xs_list *p = l;
1746            xs_dict *v;
1747            xs_list *excl = xs_dict_get(args, "exclude_types[]");
1748
1749            while (xs_list_iter(&p, &v)) {
1750                xs *noti = notify_get(&snac1, v);
1751
1752                if (noti == NULL)
1753                    continue;
1754
1755                const char *type  = xs_dict_get(noti, "type");
1756                const char *utype = xs_dict_get(noti, "utype");
1757                const char *objid = xs_dict_get(noti, "objid");
1758                xs *actor = NULL;
1759                xs *entry = NULL;
1760
1761                if (!valid_status(actor_get(xs_dict_get(noti, "actor"), &actor)))
1762                    continue;
1763
1764                if (objid != NULL && !valid_status(object_get(objid, &entry)))
1765                    continue;
1766
1767                if (is_hidden(&snac1, objid))
1768                    continue;
1769
1770                /* convert the type */
1771                if (strcmp(type, "Like") == 0)
1772                    type = "favourite";
1773                else
1774                if (strcmp(type, "Announce") == 0)
1775                    type = "reblog";
1776                else
1777                if (strcmp(type, "Follow") == 0)
1778                    type = "follow";
1779                else
1780                if (strcmp(type, "Create") == 0)
1781                    type = "mention";
1782                else
1783                if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
1784                    type = "poll";
1785                else
1786                    continue;
1787
1788                /* excluded type? */
1789                if (!xs_is_null(excl) && xs_list_in(excl, type) != -1)
1790                    continue;
1791
1792                xs *mn = xs_dict_new();
1793
1794                mn = xs_dict_append(mn, "type", type);
1795
1796                xs *id = xs_replace(xs_dict_get(noti, "id"), ".", "");
1797                mn = xs_dict_append(mn, "id", id);
1798
1799                mn = xs_dict_append(mn, "created_at", xs_dict_get(noti, "date"));
1800
1801                xs *acct = mastoapi_account(actor);
1802                mn = xs_dict_append(mn, "account", acct);
1803
1804                if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) {
1805                    xs *st = mastoapi_status(&snac1, entry);
1806
1807                    if (st)
1808                        mn = xs_dict_append(mn, "status", st);
1809                }
1810
1811                out = xs_list_append(out, mn);
1812            }
1813
1814            *body  = xs_json_dumps(out, 4);
1815            *ctype = "application/json";
1816            status = 200;
1817        }
1818        else
1819            status = 401;
1820    }
1821    else
1822    if (strcmp(cmd, "/v1/filters") == 0) { /** **/
1823        /* snac will never have filters */
1824        *body  = xs_dup("[]");
1825        *ctype = "application/json";
1826        status = 200;
1827    }
1828    else
1829    if (strcmp(cmd, "/v2/filters") == 0) { /** **/
1830        /* snac will never have filters 
1831         * but still, without a v2 endpoint a short delay is introduced
1832         * in some apps */
1833        *body  = xs_dup("[]");
1834        *ctype = "application/json";
1835        status = 200;
1836    }
1837    else
1838    if (strcmp(cmd, "/v1/favourites") == 0) { /** **/
1839        /* snac will never support a list of favourites */
1840        *body  = xs_dup("[]");
1841        *ctype = "application/json";
1842        status = 200;
1843    }
1844    else
1845    if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/
1846        /* snac does not support bookmarks */
1847        *body  = xs_dup("[]");
1848        *ctype = "application/json";
1849        status = 200;
1850    }
1851    else
1852    if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
1853        if (logged_in) {
1854            xs *lol = list_maint(&snac1, NULL, 0);
1855            xs *l   = xs_list_new();
1856            int c = 0;
1857            xs_list *li;
1858
1859            while (xs_list_next(lol, &li, &c)) {
1860                xs *d = xs_dict_new();
1861
1862                d = xs_dict_append(d, "id", xs_list_get(li, 0));
1863                d = xs_dict_append(d, "title", xs_list_get(li, 1));
1864                d = xs_dict_append(d, "replies_policy", "list");
1865                d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
1866
1867                l = xs_list_append(l, d);
1868            }
1869
1870            *body  = xs_json_dumps(l, 4);
1871            *ctype = "application/json";
1872            status = 200;
1873        }
1874    }
1875    else
1876    if (xs_startswith(cmd, "/v1/lists/")) { /** list information **/
1877        if (logged_in) {
1878            xs *l = xs_split(cmd, "/");
1879            char *p = xs_list_get(l, -1);
1880
1881            if (p) {
1882                if (strcmp(p, "accounts") == 0) {
1883                    p = xs_list_get(l, -2);
1884
1885                    if (p && xs_is_hex(p)) {
1886                        xs *actors = list_content(&snac1, p, NULL, 0);
1887                        xs *out = xs_list_new();
1888                        int c = 0;
1889                        char *v;
1890
1891                        while (xs_list_next(actors, &v, &c)) {
1892                            xs *actor = NULL;
1893
1894                            if (valid_status(object_get_by_md5(v, &actor))) {
1895                                xs *acct = mastoapi_account(actor);
1896                                out = xs_list_append(out, acct);
1897                            }
1898                        }
1899
1900                        *body  = xs_json_dumps(out, 4);
1901                        *ctype = "application/json";
1902                        status = 200;
1903                    }
1904                }
1905                else
1906                if (xs_is_hex(p)) {
1907                    xs *out = xs_list_new();
1908                    xs *lol = list_maint(&snac1, NULL, 0);
1909                    int c = 0;
1910                    xs_list *v;
1911
1912                    while (xs_list_next(lol, &v, &c)) {
1913                        char *id = xs_list_get(v, 0);
1914
1915                        if (id && strcmp(id, p) == 0) {
1916                            xs *d = xs_dict_new();
1917
1918                            d = xs_dict_append(d, "id", p);
1919                            d = xs_dict_append(d, "title", xs_list_get(v, 1));
1920                            d = xs_dict_append(d, "replies_policy", "list");
1921                            d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
1922
1923                            out = xs_list_append(out, d);
1924                            break;
1925                        }
1926                    }
1927
1928                    *body  = xs_json_dumps(out, 4);
1929                    *ctype = "application/json";
1930                    status = 200;
1931                }
1932            }
1933        }
1934    }
1935    else
1936    if (strcmp(cmd, "/v1/scheduled_statuses") == 0) { /** **/
1937        /* snac does not schedule notes */
1938        *body  = xs_dup("[]");
1939        *ctype = "application/json";
1940        status = 200;
1941    }
1942    else
1943    if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/
1944        /* snac does not support optional follow confirmations */
1945        *body  = xs_dup("[]");
1946        *ctype = "application/json";
1947        status = 200;
1948    }
1949    else
1950    if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
1951        /* snac has no announcements (yet?) */
1952        *body  = xs_dup("[]");
1953        *ctype = "application/json";
1954        status = 200;
1955    }
1956    else
1957    if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
1958        /* are you kidding me? */
1959        *body  = xs_dup("[]");
1960        *ctype = "application/json";
1961        status = 200;
1962    }
1963    else
1964    if (strcmp(cmd, "/v1/instance") == 0) { /** **/
1965        /* returns an instance object */
1966        xs *ins = xs_dict_new();
1967        const char *host  = xs_dict_get(srv_config, "host");
1968        const char *title = xs_dict_get(srv_config, "title");
1969        const char *sdesc = xs_dict_get(srv_config, "short_description");
1970
1971        ins = xs_dict_append(ins, "uri",         host);
1972        ins = xs_dict_append(ins, "domain",      host);
1973        ins = xs_dict_append(ins, "title",       title && *title ? title : host);
1974        ins = xs_dict_append(ins, "version",     "4.0.0 (not true; really " USER_AGENT ")");
1975        ins = xs_dict_append(ins, "source_url",  WHAT_IS_SNAC_URL);
1976        ins = xs_dict_append(ins, "description", host);
1977
1978        ins = xs_dict_append(ins, "short_description", sdesc && *sdesc ? sdesc : host);
1979
1980        xs *susie = xs_fmt("%s/susie.png", srv_baseurl);
1981        ins = xs_dict_append(ins, "thumbnail", susie);
1982
1983        const char *v = xs_dict_get(srv_config, "admin_email");
1984        if (xs_is_null(v) || *v == '\0')
1985            v = "admin@localhost";
1986
1987        ins = xs_dict_append(ins, "email", v);
1988
1989        ins = xs_dict_append(ins, "rules", xs_stock(XSTYPE_LIST));
1990
1991        xs *l1 = xs_list_append(xs_list_new(), "en");
1992        ins = xs_dict_append(ins, "languages", l1);
1993
1994        xs *wss = xs_fmt("wss:/" "/%s", xs_dict_get(srv_config, "host"));
1995        xs *urls = xs_dict_new();
1996        urls = xs_dict_append(urls, "streaming_api", wss);
1997
1998        ins = xs_dict_append(ins, "urls", urls);
1999
2000        xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock(0));
2001        d2 = xs_dict_append(d2, "status_count", xs_stock(0));
2002        d2 = xs_dict_append(d2, "domain_count", xs_stock(0));
2003        ins = xs_dict_append(ins, "stats", d2);
2004
2005        ins = xs_dict_append(ins, "registrations",     xs_stock(XSTYPE_FALSE));
2006        ins = xs_dict_append(ins, "approval_required", xs_stock(XSTYPE_FALSE));
2007        ins = xs_dict_append(ins, "invites_enabled",   xs_stock(XSTYPE_FALSE));
2008
2009        xs *cfg = xs_dict_new();
2010
2011        {
2012            xs *d11 = xs_json_loads("{\"characters_reserved_per_url\":32,"
2013                "\"max_characters\":100000,\"max_media_attachments\":8}");
2014            cfg = xs_dict_append(cfg, "statuses", d11);
2015
2016            xs *d12 = xs_json_loads("{\"max_featured_tags\":10}");
2017            cfg = xs_dict_append(cfg, "accounts", d12);
2018
2019            xs *d13 = xs_json_loads("{\"image_matrix_limit\":33177600,"
2020                        "\"image_size_limit\":16777216,"
2021                        "\"video_frame_rate_limit\":120,"
2022                        "\"video_matrix_limit\":8294400,"
2023                        "\"video_size_limit\":103809024}"
2024            );
2025
2026            {
2027                /* get the supported mime types from the internal list */
2028                const char **p = xs_mime_types;
2029                xs_set mtypes;
2030                xs_set_init(&mtypes);
2031
2032                while (*p) {
2033                    const char *type = p[1];
2034
2035                    if (xs_startswith(type, "image/") ||
2036                        xs_startswith(type, "video/") ||
2037                        xs_startswith(type, "audio/"))
2038                        xs_set_add(&mtypes, type);
2039
2040                    p += 2;
2041                }
2042
2043                xs *l = xs_set_result(&mtypes);
2044                d13 = xs_dict_append(d13, "supported_mime_types", l);
2045            }
2046
2047            cfg = xs_dict_append(cfg, "media_attachments", d13);
2048
2049            xs *d14 = xs_json_loads("{\"max_characters_per_option\":50,"
2050                "\"max_expiration\":2629746,"
2051                "\"max_options\":8,\"min_expiration\":300}");
2052            cfg = xs_dict_append(cfg, "polls", d14);
2053        }
2054
2055        ins = xs_dict_append(ins, "configuration", cfg);
2056
2057        const char *admin_account = xs_dict_get(srv_config, "admin_account");
2058
2059        if (!xs_is_null(admin_account) && *admin_account) {
2060            snac admin;
2061
2062            if (user_open(&admin, admin_account)) {
2063                xs *actor = msg_actor(&admin);
2064                xs *acct  = mastoapi_account(actor);
2065
2066                ins = xs_dict_append(ins, "contact_account", acct);
2067
2068                user_free(&admin);
2069            }
2070        }
2071
2072        *body  = xs_json_dumps(ins, 4);
2073        *ctype = "application/json";
2074        status = 200;
2075    }
2076    else
2077    if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
2078        /* information about a status */
2079        if (logged_in) {
2080            xs *l = xs_split(cmd, "/");
2081            const char *id = xs_list_get(l, 3);
2082            const char *op = xs_list_get(l, 4);
2083
2084            if (!xs_is_null(id)) {
2085                xs *msg = NULL;
2086                xs *out = NULL;
2087
2088                /* skip the 'fake' part of the id */
2089                id = MID_TO_MD5(id);
2090
2091                if (valid_status(object_get_by_md5(id, &msg))) {
2092                    if (op == NULL) {
2093                        if (!is_muted(&snac1, get_atto(msg))) {
2094                            /* return the status itself */
2095                            out = mastoapi_status(&snac1, msg);
2096                        }
2097                    }
2098                    else
2099                    if (strcmp(op, "context") == 0) { /** **/
2100                        /* return ancestors and children */
2101                        xs *anc = xs_list_new();
2102                        xs *des = xs_list_new();
2103                        xs_list *p;
2104                        xs_str *v;
2105                        char pid[64];
2106
2107                        /* build the [grand]parent list, moving up */
2108                        strncpy(pid, id, sizeof(pid));
2109
2110                        while (object_parent(pid, pid, sizeof(pid))) {
2111                            xs *m2 = NULL;
2112
2113                            if (valid_status(timeline_get_by_md5(&snac1, pid, &m2))) {
2114                                xs *st = mastoapi_status(&snac1, m2);
2115
2116                                if (st)
2117                                    anc = xs_list_insert(anc, 0, st);
2118                            }
2119                            else
2120                                break;
2121                        }
2122
2123                        /* build the children list */
2124                        xs *children = object_children(xs_dict_get(msg, "id"));
2125                        p = children;
2126
2127                        while (xs_list_iter(&p, &v)) {
2128                            xs *m2 = NULL;
2129
2130                            if (valid_status(timeline_get_by_md5(&snac1, v, &m2))) {
2131                                if (xs_is_null(xs_dict_get(m2, "name"))) {
2132                                    xs *st = mastoapi_status(&snac1, m2);
2133
2134                                    if (st)
2135                                        des = xs_list_append(des, st);
2136                                }
2137                            }
2138                        }
2139
2140                        out = xs_dict_new();
2141                        out = xs_dict_append(out, "ancestors",   anc);
2142                        out = xs_dict_append(out, "descendants", des);
2143                    }
2144                    else
2145                    if (strcmp(op, "reblogged_by") == 0 || /** **/
2146                        strcmp(op, "favourited_by") == 0) { /** **/
2147                        /* return the list of people who liked or boosted this */
2148                        out = xs_list_new();
2149
2150                        xs *l = NULL;
2151
2152                        if (op[0] == 'r')
2153                            l = object_announces(xs_dict_get(msg, "id"));
2154                        else
2155                            l = object_likes(xs_dict_get(msg, "id"));
2156
2157                        xs_list *p = l;
2158                        xs_str *v;
2159
2160                        while (xs_list_iter(&p, &v)) {
2161                            xs *actor2 = NULL;
2162
2163                            if (valid_status(object_get_by_md5(v, &actor2))) {
2164                                xs *acct2 = mastoapi_account(actor2);
2165
2166                                out = xs_list_append(out, acct2);
2167                            }
2168                        }
2169                    }
2170                    else
2171                    if (strcmp(op, "source") == 0) { /** **/
2172                        out = xs_dict_new();
2173
2174                        /* get the mastoapi status id */
2175                        out = xs_dict_append(out, "id", xs_list_get(l, 3));
2176
2177                        out = xs_dict_append(out, "text", xs_dict_get(msg, "sourceContent"));
2178                        out = xs_dict_append(out, "spoiler_text", xs_dict_get(msg, "summary"));
2179                    }
2180                }
2181                else
2182                    srv_debug(1, xs_fmt("mastoapi status: bad id %s", id));
2183
2184                if (out != NULL) {
2185                    *body  = xs_json_dumps(out, 4);
2186                    *ctype = "application/json";
2187                    status = 200;
2188                }
2189            }
2190        }
2191        else
2192            status = 401;
2193    }
2194    else
2195    if (strcmp(cmd, "/v1/preferences") == 0) { /** **/
2196        *body  = xs_dup("{}");
2197        *ctype = "application/json";
2198        status = 200;
2199    }
2200    else
2201    if (strcmp(cmd, "/v1/markers") == 0) { /** **/
2202        *body  = xs_dup("{}");
2203        *ctype = "application/json";
2204        status = 200;
2205    }
2206    else
2207    if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
2208        *body  = xs_dup("[]");
2209        *ctype = "application/json";
2210        status = 200;
2211    }
2212    else
2213    if (strcmp(cmd, "/v2/search") == 0) { /** **/
2214        if (logged_in) {
2215            const char *q      = xs_dict_get(args, "q");
2216            const char *type   = xs_dict_get(args, "type");
2217            const char *offset = xs_dict_get(args, "offset");
2218
2219            xs *acl = xs_list_new();
2220            xs *stl = xs_list_new();
2221            xs *htl = xs_list_new();
2222            xs *res = xs_dict_new();
2223
2224            if (xs_is_null(offset) || strcmp(offset, "0") == 0) {
2225                /* reply something only for offset 0; otherwise,
2226                   apps like Tusky keep asking again and again */
2227
2228                if (!xs_is_null(q)) {
2229                    if (xs_is_null(type) || strcmp(type, "accounts") == 0) {
2230                        /* do a webfinger query */
2231                        char *actor = NULL;
2232                        char *user  = NULL;
2233
2234                        if (valid_status(webfinger_request(q, &actor, &user)) && actor) {
2235                            xs *actor_o = NULL;
2236
2237                            if (valid_status(actor_request(&snac1, actor, &actor_o))) {
2238                                xs *acct = mastoapi_account(actor_o);
2239
2240                                acl = xs_list_append(acl, acct);
2241                            }
2242                        }
2243                    }
2244
2245                    if (xs_is_null(type) || strcmp(type, "hashtags") == 0) {
2246                        /* search this tag */
2247                        xs *tl = tag_search((char *)q, 0, 1);
2248
2249                        if (xs_list_len(tl)) {
2250                            xs *d = xs_dict_new();
2251
2252                            d = xs_dict_append(d, "name", q);
2253                            xs *url = xs_fmt("%s?t=%s", srv_baseurl, q);
2254                            d = xs_dict_append(d, "url", url);
2255                            d = xs_dict_append(d, "history", xs_stock(XSTYPE_LIST));
2256
2257                            htl = xs_list_append(htl, d);
2258                        }
2259                    }
2260
2261                    if (xs_is_null(type) || strcmp(type, "statuses") == 0) {
2262                        int to = 0;
2263                        int cnt = 40;
2264                        xs *tl = content_search(&snac1, q, 1, 0, cnt, 0, &to);
2265                        int c = 0;
2266                        char *v;
2267
2268                        while (xs_list_next(tl, &v, &c) && --cnt) {
2269                            xs *post = NULL;
2270
2271                            if (!valid_status(timeline_get_by_md5(&snac1, v, &post)))
2272                                continue;
2273
2274                            xs *s = mastoapi_status(&snac1, post);
2275
2276                            if (!xs_is_null(s))
2277                                stl = xs_list_append(stl, s);
2278                        }
2279                    }
2280                }
2281            }
2282
2283            res = xs_dict_append(res, "accounts", acl);
2284            res = xs_dict_append(res, "statuses", stl);
2285            res = xs_dict_append(res, "hashtags", htl);
2286
2287            *body  = xs_json_dumps(res, 4);
2288            *ctype = "application/json";
2289            status = 200;
2290        }
2291        else
2292            status = 401;
2293    }
2294
2295    /* user cleanup */
2296    if (logged_in)
2297        user_free(&snac1);
2298
2299    srv_debug(1, xs_fmt("mastoapi_get_handler %s %d", q_path, status));
2300
2301    return status;
2302}
2303
2304
2305int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2306                          const char *payload, int p_size,
2307                          char **body, int *b_size, char **ctype)
2308{
2309    (void)p_size;
2310    (void)b_size;
2311
2312    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2313        return 0;
2314
2315    int status    = 404;
2316    xs *args      = NULL;
2317    char *i_ctype = xs_dict_get(req, "content-type");
2318
2319    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
2320        if (!xs_is_null(payload))
2321            args = xs_json_loads(payload);
2322    }
2323    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
2324    {
2325        // Some apps send form data instead of json so we should cater for those
2326        if (!xs_is_null(payload)) {
2327            xs *upl = xs_url_dec(payload);
2328            args    = xs_url_vars(upl);
2329        }
2330    }
2331    else
2332        args = xs_dup(xs_dict_get(req, "p_vars"));
2333
2334    if (args == NULL)
2335        return 400;
2336
2337    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
2338
2339    snac snac = {0};
2340    int logged_in = process_auth_token(&snac, req);
2341
2342    if (strcmp(cmd, "/v1/apps") == 0) { /** **/
2343        const char *name  = xs_dict_get(args, "client_name");
2344        const char *ruri  = xs_dict_get(args, "redirect_uris");
2345        const char *scope = xs_dict_get(args, "scope");
2346
2347        if (xs_type(ruri) == XSTYPE_LIST)
2348            ruri = xs_dict_get(ruri, 0);
2349
2350        if (name && ruri) {
2351            xs *app  = xs_dict_new();
2352            xs *id   = xs_replace_i(tid(0), ".", "");
2353            xs *csec = random_str();
2354            xs *vkey = random_str();
2355            xs *cid  = NULL;
2356
2357            /* pick a non-existent random cid */
2358            for (;;) {
2359                cid = random_str();
2360                xs *p_app = app_get(cid);
2361
2362                if (p_app == NULL)
2363                    break;
2364
2365                xs_free(cid);
2366            }
2367
2368            app = xs_dict_append(app, "name",          name);
2369            app = xs_dict_append(app, "redirect_uri",  ruri);
2370            app = xs_dict_append(app, "client_id",     cid);
2371            app = xs_dict_append(app, "client_secret", csec);
2372            app = xs_dict_append(app, "vapid_key",     vkey);
2373            app = xs_dict_append(app, "id",            id);
2374
2375            *body  = xs_json_dumps(app, 4);
2376            *ctype = "application/json";
2377            status = 200;
2378
2379            app = xs_dict_append(app, "code", "");
2380
2381            if (scope)
2382                app = xs_dict_append(app, "scope", scope);
2383
2384            app_add(cid, app);
2385
2386            srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
2387        }
2388    }
2389    else
2390    if (strcmp(cmd, "/v1/statuses") == 0) { /** **/
2391        if (logged_in) {
2392            /* post a new Note */
2393            const char *content    = xs_dict_get(args, "status");
2394            const char *mid        = xs_dict_get(args, "in_reply_to_id");
2395            const char *visibility = xs_dict_get(args, "visibility");
2396            const char *summary    = xs_dict_get(args, "spoiler_text");
2397            const char *media_ids  = xs_dict_get(args, "media_ids");
2398
2399            if (xs_is_null(media_ids))
2400                media_ids = xs_dict_get(args, "media_ids[]");
2401
2402            if (xs_is_null(media_ids))
2403                media_ids = xs_dict_get(args, "media_ids");
2404
2405            if (xs_is_null(visibility))
2406                visibility = "public";
2407
2408            xs *attach_list = xs_list_new();
2409            xs *irt         = NULL;
2410
2411            /* is it a reply? */
2412            if (mid != NULL) {
2413                xs *r_msg = NULL;
2414                const char *md5 = MID_TO_MD5(mid);
2415
2416                if (valid_status(object_get_by_md5(md5, &r_msg)))
2417                    irt = xs_dup(xs_dict_get(r_msg, "id"));
2418            }
2419
2420            /* does it have attachments? */
2421            if (!xs_is_null(media_ids)) {
2422                xs *mi = NULL;
2423
2424                if (xs_type(media_ids) == XSTYPE_LIST)
2425                    mi = xs_dup(media_ids);
2426                else {
2427                    mi = xs_list_new();
2428                    mi = xs_list_append(mi, media_ids);
2429                }
2430
2431                xs_list *p = mi;
2432                xs_str *v;
2433
2434                while (xs_list_iter(&p, &v)) {
2435                    xs *l    = xs_list_new();
2436                    xs *url  = xs_fmt("%s/s/%s", snac.actor, v);
2437                    xs *desc = static_get_meta(&snac, v);
2438
2439                    l = xs_list_append(l, url);
2440                    l = xs_list_append(l, desc);
2441
2442                    attach_list = xs_list_append(attach_list, l);
2443                }
2444            }
2445
2446            /* prepare the message */
2447            xs *msg = msg_note(&snac, content, NULL, irt, attach_list,
2448                        strcmp(visibility, "public") == 0 ? 0 : 1);
2449
2450            if (!xs_is_null(summary) && *summary) {
2451                msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
2452                msg = xs_dict_set(msg, "summary",   summary);
2453            }
2454
2455            /* store */
2456            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
2457
2458            /* 'Create' message */
2459            xs *c_msg = msg_create(&snac, msg);
2460            enqueue_message(&snac, c_msg);
2461
2462            timeline_touch(&snac);
2463
2464            /* convert to a mastodon status as a response code */
2465            xs *st = mastoapi_status(&snac, msg);
2466
2467            *body  = xs_json_dumps(st, 4);
2468            *ctype = "application/json";
2469            status = 200;
2470        }
2471        else
2472            status = 401;
2473    }
2474    else
2475    if (xs_startswith(cmd, "/v1/statuses")) { /** **/
2476        if (logged_in) {
2477            /* operations on a status */
2478            xs *l = xs_split(cmd, "/");
2479            const char *mid = xs_list_get(l, 3);
2480            const char *op  = xs_list_get(l, 4);
2481
2482            if (!xs_is_null(mid)) {
2483                xs *msg = NULL;
2484                xs *out = NULL;
2485
2486                /* skip the 'fake' part of the id */
2487                mid = MID_TO_MD5(mid);
2488
2489                if (valid_status(timeline_get_by_md5(&snac, mid, &msg))) {
2490                    char *id = xs_dict_get(msg, "id");
2491
2492                    if (op == NULL) {
2493                        /* no operation (?) */
2494                    }
2495                    else
2496                    if (strcmp(op, "favourite") == 0) { /** **/
2497                        xs *n_msg = msg_admiration(&snac, id, "Like");
2498
2499                        if (n_msg != NULL) {
2500                            enqueue_message(&snac, n_msg);
2501                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1);
2502
2503                            out = mastoapi_status(&snac, msg);
2504                        }
2505                    }
2506                    else
2507                    if (strcmp(op, "unfavourite") == 0) { /** **/
2508                        xs *n_msg = msg_repulsion(&snac, id, "Like");
2509
2510                        if (n_msg != NULL) {
2511                            enqueue_message(&snac, n_msg);
2512
2513                            out = mastoapi_status(&snac, msg);
2514                        }
2515                    }
2516                    else
2517                    if (strcmp(op, "reblog") == 0) { /** **/
2518                        xs *n_msg = msg_admiration(&snac, id, "Announce");
2519
2520                        if (n_msg != NULL) {
2521                            enqueue_message(&snac, n_msg);
2522                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0);
2523
2524                            out = mastoapi_status(&snac, msg);
2525                        }
2526                    }
2527                    else
2528                    if (strcmp(op, "unreblog") == 0) { /** **/
2529                        xs *n_msg = msg_repulsion(&snac, id, "Announce");
2530
2531                        if (n_msg != NULL) {
2532                            enqueue_message(&snac, n_msg);
2533
2534                            out = mastoapi_status(&snac, msg);
2535                        }
2536                    }
2537                    else
2538                    if (strcmp(op, "bookmark") == 0) { /** **/
2539                        /* snac does not support bookmarks */
2540                    }
2541                    else
2542                    if (strcmp(op, "unbookmark") == 0) { /** **/
2543                        /* snac does not support bookmarks */
2544                    }
2545                    else
2546                    if (strcmp(op, "pin") == 0) { /** **/
2547                        /* pin this message */
2548                        if (pin(&snac, id))
2549                            out = mastoapi_status(&snac, msg);
2550                        else
2551                            status = 422;
2552                    }
2553                    else
2554                    if (strcmp(op, "unpin") == 0) { /** **/
2555                        /* unpin this message */
2556                        unpin(&snac, id);
2557                        out = mastoapi_status(&snac, msg);
2558                    }
2559                    else
2560                    if (strcmp(op, "mute") == 0) { /** **/
2561                        /* Mastodon's mute is snac's hide */
2562                    }
2563                    else
2564                    if (strcmp(op, "unmute") == 0) { /** **/
2565                        /* Mastodon's unmute is snac's unhide */
2566                    }
2567                }
2568
2569                if (out != NULL) {
2570                    *body  = xs_json_dumps(out, 4);
2571                    *ctype = "application/json";
2572                    status = 200;
2573                }
2574            }
2575        }
2576        else
2577            status = 401;
2578    }
2579    else
2580    if (strcmp(cmd, "/v1/notifications/clear") == 0) { /** **/
2581        if (logged_in) {
2582            notify_clear(&snac);
2583            timeline_touch(&snac);
2584
2585            *body  = xs_dup("{}");
2586            *ctype = "application/json";
2587            status = 200;
2588        }
2589        else
2590            status = 401;
2591    }
2592    else
2593    if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
2594        /* I don't know what I'm doing */
2595        if (logged_in) {
2596            char *v;
2597
2598            xs *wpush = xs_dict_new();
2599
2600            wpush = xs_dict_append(wpush, "id", "1");
2601
2602            v = xs_dict_get(args, "data");
2603            v = xs_dict_get(v, "alerts");
2604            wpush = xs_dict_append(wpush, "alerts", v);
2605
2606            v = xs_dict_get(args, "subscription");
2607            v = xs_dict_get(v, "endpoint");
2608            wpush = xs_dict_append(wpush, "endpoint", v);
2609
2610            xs *server_key = random_str();
2611            wpush = xs_dict_append(wpush, "server_key", server_key);
2612
2613            *body  = xs_json_dumps(wpush, 4);
2614            *ctype = "application/json";
2615            status = 200;
2616        }
2617        else
2618            status = 401;
2619    }
2620    else
2621    if (strcmp(cmd, "/v1/media") == 0 || strcmp(cmd, "/v2/media") == 0) { /** **/
2622        if (logged_in) {
2623            const xs_list *file = xs_dict_get(args, "file");
2624            const char *desc    = xs_dict_get(args, "description");
2625
2626            if (xs_is_null(desc))
2627                desc = "";
2628
2629            status = 400;
2630
2631            if (xs_type(file) == XSTYPE_LIST) {
2632                const char *fn = xs_list_get(file, 0);
2633
2634                if (*fn != '\0') {
2635                    char *ext = strrchr(fn, '.');
2636                    xs *hash  = xs_md5_hex(fn, strlen(fn));
2637                    xs *id    = xs_fmt("%s%s", hash, ext);
2638                    xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
2639                    int fo    = xs_number_get(xs_list_get(file, 1));
2640                    int fs    = xs_number_get(xs_list_get(file, 2));
2641
2642                    /* store */
2643                    static_put(&snac, id, payload + fo, fs);
2644                    static_put_meta(&snac, id, desc);
2645
2646                    /* prepare a response */
2647                    xs *rsp = xs_dict_new();
2648
2649                    rsp = xs_dict_append(rsp, "id",          id);
2650                    rsp = xs_dict_append(rsp, "type",        "image");
2651                    rsp = xs_dict_append(rsp, "url",         url);
2652                    rsp = xs_dict_append(rsp, "preview_url", url);
2653                    rsp = xs_dict_append(rsp, "remote_url",  url);
2654                    rsp = xs_dict_append(rsp, "description", desc);
2655
2656                    *body  = xs_json_dumps(rsp, 4);
2657                    *ctype = "application/json";
2658                    status = 200;
2659                }
2660            }
2661        }
2662        else
2663            status = 401;
2664    }
2665    else
2666    if (xs_startswith(cmd, "/v1/accounts")) { /** **/
2667        if (logged_in) {
2668            /* account-related information */
2669            xs *l = xs_split(cmd, "/");
2670            const char *md5 = xs_list_get(l, 3);
2671            const char *opt = xs_list_get(l, 4);
2672            xs *rsp = NULL;
2673
2674            if (!xs_is_null(md5) && *md5) {
2675                xs *actor_o = NULL;
2676
2677                if (xs_is_null(opt)) {
2678                    /* ? */
2679                }
2680                else
2681                if (strcmp(opt, "follow") == 0) { /** **/
2682                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
2683                        const char *actor = xs_dict_get(actor_o, "id");
2684
2685                        xs *msg = msg_follow(&snac, actor);
2686
2687                        if (msg != NULL) {
2688                            /* reload the actor from the message, in may be different */
2689                            actor = xs_dict_get(msg, "object");
2690
2691                            following_add(&snac, actor, msg);
2692
2693                            enqueue_output_by_actor(&snac, msg, actor, 0);
2694
2695                            rsp = mastoapi_relationship(&snac, md5);
2696                        }
2697                    }
2698                }
2699                else
2700                if (strcmp(opt, "unfollow") == 0) { /** **/
2701                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
2702                        const char *actor = xs_dict_get(actor_o, "id");
2703
2704                        /* get the following object */
2705                        xs *object = NULL;
2706
2707                        if (valid_status(following_get(&snac, actor, &object))) {
2708                            xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
2709
2710                            following_del(&snac, actor);
2711
2712                            enqueue_output_by_actor(&snac, msg, actor, 0);
2713
2714                            rsp = mastoapi_relationship(&snac, md5);
2715                        }
2716                    }
2717                }
2718                else
2719                if (strcmp(opt, "block") == 0) { /** **/
2720                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
2721                        const char *actor = xs_dict_get(actor_o, "id");
2722
2723                        mute(&snac, actor);
2724
2725                        rsp = mastoapi_relationship(&snac, md5);
2726                    }
2727                }
2728                else
2729                if (strcmp(opt, "unblock") == 0) { /** **/
2730                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
2731                        const char *actor = xs_dict_get(actor_o, "id");
2732
2733                        unmute(&snac, actor);
2734
2735                        rsp = mastoapi_relationship(&snac, md5);
2736                    }
2737                }
2738            }
2739
2740            if (rsp != NULL) {
2741                *body  = xs_json_dumps(rsp, 4);
2742                *ctype = "application/json";
2743                status = 200;
2744            }
2745        }
2746        else
2747            status = 401;
2748    }
2749    else
2750    if (xs_startswith(cmd, "/v1/polls")) { /** **/
2751        if (logged_in) {
2752            /* operations on a status */
2753            xs *l = xs_split(cmd, "/");
2754            const char *mid = xs_list_get(l, 3);
2755            const char *op  = xs_list_get(l, 4);
2756
2757            if (!xs_is_null(mid)) {
2758                xs *msg = NULL;
2759                xs *out = NULL;
2760
2761                /* skip the 'fake' part of the id */
2762                mid = MID_TO_MD5(mid);
2763
2764                if (valid_status(timeline_get_by_md5(&snac, mid, &msg))) {
2765                    const char *id   = xs_dict_get(msg, "id");
2766                    const char *atto = get_atto(msg);
2767
2768                    xs_list *opts = xs_dict_get(msg, "oneOf");
2769                    if (opts == NULL)
2770                        opts = xs_dict_get(msg, "anyOf");
2771
2772                    if (op == NULL) {
2773                    }
2774                    else
2775                    if (strcmp(op, "votes") == 0) {
2776                        xs_list *choices = xs_dict_get(args, "choices[]");
2777
2778                        if (xs_is_null(choices))
2779                            choices = xs_dict_get(args, "choices");
2780
2781                        if (xs_type(choices) == XSTYPE_LIST) {
2782                            xs_str *v;
2783
2784                            while (xs_list_iter(&choices, &v)) {
2785                                int io           = atoi(v);
2786                                const xs_dict *o = xs_list_get(opts, io);
2787
2788                                if (o) {
2789                                    const char *name = xs_dict_get(o, "name");
2790
2791                                    xs *msg = msg_note(&snac, "", atto, (char *)id, NULL, 1);
2792                                    msg = xs_dict_append(msg, "name", name);
2793
2794                                    xs *c_msg = msg_create(&snac, msg);
2795                                    enqueue_message(&snac, c_msg);
2796                                    timeline_add(&snac, xs_dict_get(msg, "id"), msg);
2797                                }
2798                            }
2799
2800                            out = mastoapi_poll(&snac, msg);
2801                        }
2802                    }
2803                }
2804
2805                if (out != NULL) {
2806                    *body  = xs_json_dumps(out, 4);
2807                    *ctype = "application/json";
2808                    status = 200;
2809                }
2810            }
2811        }
2812        else
2813            status = 401;
2814    }
2815    else
2816    if (strcmp(cmd, "/v1/lists") == 0) {
2817        if (logged_in) {
2818            const char *title = xs_dict_get(args, "title");
2819
2820            if (xs_type(title) == XSTYPE_STRING) {
2821                /* add the list */
2822                xs *out = xs_dict_new();
2823
2824                if (xs_type(list_maint(&snac, title, 1)) == XSTYPE_TRUE) {
2825                    out = xs_dict_append(out, "title", title);
2826                    out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
2827                    out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
2828
2829                    status = 200;
2830                }
2831                else {
2832                    out = xs_dict_append(out, "error", "cannot create list");
2833                    status = 422;
2834                }
2835
2836                *body  = xs_json_dumps(out, 4);
2837                *ctype = "application/json";
2838            }
2839            else
2840                status = 422;
2841        }
2842    }
2843    if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
2844        if (logged_in) {
2845            xs *l = xs_split(cmd, "/");
2846            char *op = xs_list_get(l, -1);
2847            char *id = xs_list_get(l, -2);
2848
2849            if (op && id && xs_is_hex(id)) {
2850                if (strcmp(op, "accounts") == 0) {
2851                    xs_list *accts = xs_dict_get(args, "account_ids[]");
2852                    int c = 0;
2853                    char *v;
2854
2855                    while (xs_list_next(accts, &v, &c)) {
2856                        list_content(&snac, id, v, 1);
2857                    }
2858
2859                    status = 200;
2860                }
2861            }
2862        }
2863        else
2864            status = 422;
2865    }
2866
2867    /* user cleanup */
2868    if (logged_in)
2869        user_free(&snac);
2870
2871    srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status));
2872
2873    return status;
2874}
2875
2876
2877int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
2878                          const char *payload, int p_size,
2879                          char **body, int *b_size, char **ctype)
2880{
2881    (void)p_size;
2882    (void)body;
2883    (void)b_size;
2884    (void)ctype;
2885
2886    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2887        return 0;
2888
2889    int status    = 404;
2890    xs *args      = NULL;
2891    char *i_ctype = xs_dict_get(req, "content-type");
2892
2893    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
2894        if (!xs_is_null(payload))
2895            args = xs_json_loads(payload);
2896    }
2897    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
2898    {
2899        // Some apps send form data instead of json so we should cater for those
2900        if (!xs_is_null(payload)) {
2901            xs *upl = xs_url_dec(payload);
2902            args    = xs_url_vars(upl);
2903        }
2904    }
2905    else
2906        args = xs_dup(xs_dict_get(req, "p_vars"));
2907
2908    if (args == NULL)
2909        return 400;
2910
2911    snac snac = {0};
2912    int logged_in = process_auth_token(&snac, req);
2913
2914    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
2915
2916    if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
2917        // pretend we deleted it, since it doesn't exist anyway
2918        status = 200;
2919    }
2920    else
2921    if (xs_startswith(cmd, "/v1/lists/")) {
2922        if (logged_in) {
2923            xs *l = xs_split(cmd, "/");
2924            char *p = xs_list_get(l, -1);
2925
2926            if (p) {
2927                if (strcmp(p, "accounts") == 0) {
2928                    /* delete account from list */
2929                    p = xs_list_get(l, -2);
2930                    xs_list *accts = xs_dict_get(args, "account_ids[]");
2931                    int c = 0;
2932                    char *v;
2933
2934                    while (xs_list_next(accts, &v, &c)) {
2935                        list_content(&snac, p, v, 2);
2936                    }
2937                }
2938                else {
2939                    /* delete list */
2940                    if (xs_is_hex(p)) {
2941                        list_maint(&snac, p, 2);
2942                    }
2943                }
2944            }
2945
2946            status = 200;
2947        }
2948        else
2949            status = 401;
2950    }
2951
2952    /* user cleanup */
2953    if (logged_in)
2954        user_free(&snac);
2955
2956    srv_debug(1, xs_fmt("mastoapi_delete_handler %s %d", q_path, status));
2957
2958    return status;
2959}
2960
2961
2962int mastoapi_put_handler(const xs_dict *req, const char *q_path,
2963                          const char *payload, int p_size,
2964                          char **body, int *b_size, char **ctype)
2965{
2966    (void)p_size;
2967    (void)b_size;
2968
2969    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2970        return 0;
2971
2972    int status    = 404;
2973    xs *args      = NULL;
2974    char *i_ctype = xs_dict_get(req, "content-type");
2975
2976    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
2977        if (!xs_is_null(payload))
2978            args = xs_json_loads(payload);
2979    }
2980    else
2981        args = xs_dup(xs_dict_get(req, "p_vars"));
2982
2983    if (args == NULL)
2984        return 400;
2985
2986    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
2987
2988    snac snac = {0};
2989    int logged_in = process_auth_token(&snac, req);
2990
2991    if (xs_startswith(cmd, "/v1/media") || xs_startswith(cmd, "/v2/media")) { /** **/
2992        if (logged_in) {
2993            xs *l = xs_split(cmd, "/");
2994            const char *stid = xs_list_get(l, 3);
2995
2996            if (!xs_is_null(stid)) {
2997                const char *desc = xs_dict_get(args, "description");
2998
2999                /* set the image metadata */
3000                static_put_meta(&snac, stid, desc);
3001
3002                /* prepare a response */
3003                xs *rsp = xs_dict_new();
3004                xs *url = xs_fmt("%s/s/%s", snac.actor, stid);
3005
3006                rsp = xs_dict_append(rsp, "id",          stid);
3007                rsp = xs_dict_append(rsp, "type",        "image");
3008                rsp = xs_dict_append(rsp, "url",         url);
3009                rsp = xs_dict_append(rsp, "preview_url", url);
3010                rsp = xs_dict_append(rsp, "remote_url",  url);
3011                rsp = xs_dict_append(rsp, "description", desc);
3012
3013                *body  = xs_json_dumps(rsp, 4);
3014                *ctype = "application/json";
3015                status = 200;
3016            }
3017        }
3018        else
3019            status = 401;
3020    }
3021    else
3022    if (xs_startswith(cmd, "/v1/statuses")) {
3023        if (logged_in) {
3024            xs *l = xs_split(cmd, "/");
3025            const char *mid = xs_list_get(l, 3);
3026
3027            if (!xs_is_null(mid)) {
3028                const char *md5 = MID_TO_MD5(mid);
3029                xs *rsp = NULL;
3030                xs *msg = NULL;
3031
3032                if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) {
3033                    const char *content = xs_dict_get(args, "status");
3034                    xs *atls = xs_list_new();
3035                    xs *f_content = not_really_markdown(content, &atls, NULL);
3036
3037                    /* replace fields with new content */
3038                    msg = xs_dict_set(msg, "sourceContent", content);
3039                    msg = xs_dict_set(msg, "content", f_content);
3040
3041                    xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
3042                    msg = xs_dict_set(msg, "updated", updated);
3043
3044                    /* overwrite object, not updating the indexes */
3045                    object_add_ow(xs_dict_get(msg, "id"), msg);
3046
3047                    /* update message */
3048                    xs *c_msg = msg_update(&snac, msg);
3049
3050                    enqueue_message(&snac, c_msg);
3051
3052                    rsp = mastoapi_status(&snac, msg);
3053                }
3054
3055                if (rsp != NULL) {
3056                    *body  = xs_json_dumps(rsp, 4);
3057                    *ctype = "application/json";
3058                    status = 200;
3059                }
3060            }
3061        }
3062        else
3063            status = 401;
3064    }
3065
3066    /* user cleanup */
3067    if (logged_in)
3068        user_free(&snac);
3069
3070    srv_debug(1, xs_fmt("mastoapi_put_handler %s %d", q_path, status));
3071
3072    return status;
3073}
3074
3075
3076void mastoapi_purge(void)
3077{
3078    xs *spec   = xs_fmt("%s/app/" "*.json", srv_basedir);
3079    xs *files  = xs_glob(spec, 1, 0);
3080    xs_list *p = files;
3081    xs_str *v;
3082
3083    time_t mt = time(NULL) - 3600;
3084
3085    while (xs_list_iter(&p, &v)) {
3086        xs *cid = xs_replace(v, ".json", "");
3087        xs *fn  = _app_fn(cid);
3088
3089        if (mtime(fn) < mt) {
3090            /* get the app */
3091            xs *app = app_get(cid);
3092
3093            if (app) {
3094                /* old apps with no uid are incomplete cruft */
3095                const char *uid = xs_dict_get(app, "uid");
3096
3097                if (xs_is_null(uid) || *uid == '\0') {
3098                    unlink(fn);
3099                    srv_debug(2, xs_fmt("purged %s", fn));
3100                }
3101            }
3102        }
3103    }
3104}
3105
3106
3107#endif /* #ifndef NO_MASTODON_API */