2.85 mastoapi.c
   1/* snac - A simple, minimalistic ActivityPub instance */
   2/* copyright (c) 2022 - 2025 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#include "xs_unicode.h"
  19#include "xs_http.h"
  20
  21#include "snac.h"
  22
  23#include <sys/time.h>
  24
  25static xs_str *random_str(void)
  26/* just what is says in the tin */
  27{
  28    unsigned int data[4] = {0};
  29
  30    xs_rnd_buf(data, sizeof(data));
  31    return xs_hex_enc((char *)data, sizeof(data));
  32}
  33
  34
  35int app_add(const char *id, const xs_dict *app)
  36/* stores an app */
  37{
  38    if (!xs_is_hex(id))
  39        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
  40
  41    int status = HTTP_STATUS_CREATED;
  42    xs *fn     = xs_fmt("%s/app/", srv_basedir);
  43    FILE *f;
  44
  45    mkdirx(fn);
  46    fn = xs_str_cat(fn, id);
  47    fn = xs_str_cat(fn, ".json");
  48
  49    if ((f = fopen(fn, "w")) != NULL) {
  50        xs_json_dump(app, 4, f);
  51        fclose(f);
  52    }
  53    else
  54        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
  55
  56    return status;
  57}
  58
  59
  60xs_str *_app_fn(const char *id)
  61{
  62    return xs_fmt("%s/app/%s.json", srv_basedir, id);
  63}
  64
  65
  66xs_dict *app_get(const char *id)
  67/* gets an app */
  68{
  69    if (!xs_is_hex(id))
  70        return NULL;
  71
  72    xs *fn       = _app_fn(id);
  73    xs_dict *app = NULL;
  74    FILE *f;
  75
  76    if ((f = fopen(fn, "r")) != NULL) {
  77        app = xs_json_load(f);
  78        fclose(f);
  79
  80    }
  81
  82    return app;
  83}
  84
  85
  86int app_del(const char *id)
  87/* deletes an app */
  88{
  89    if (!xs_is_hex(id))
  90        return -1;
  91
  92    xs *fn = _app_fn(id);
  93
  94    return unlink(fn);
  95}
  96
  97
  98int token_add(const char *id, const xs_dict *token)
  99/* stores a token */
 100{
 101    if (!xs_is_hex(id))
 102        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
 103
 104    int status = HTTP_STATUS_CREATED;
 105    xs *fn     = xs_fmt("%s/token/", srv_basedir);
 106    FILE *f;
 107
 108    mkdirx(fn);
 109    fn = xs_str_cat(fn, id);
 110    fn = xs_str_cat(fn, ".json");
 111
 112    if ((f = fopen(fn, "w")) != NULL) {
 113        xs_json_dump(token, 4, f);
 114        fclose(f);
 115    }
 116    else
 117        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 118
 119    return status;
 120}
 121
 122
 123xs_dict *token_get(const char *id)
 124/* gets a token */
 125{
 126    if (!xs_is_hex(id))
 127        return NULL;
 128
 129    xs *fn         = xs_fmt("%s/token/%s.json", srv_basedir, id);
 130    xs_dict *token = NULL;
 131    FILE *f;
 132
 133    if ((f = fopen(fn, "r")) != NULL) {
 134        token = xs_json_load(f);
 135        fclose(f);
 136
 137        /* 'touch' the file */
 138        utimes(fn, NULL);
 139
 140        /* also 'touch' the app */
 141        const char *app_id = xs_dict_get(token, "client_id");
 142
 143        if (app_id) {
 144            xs *afn = xs_fmt("%s/app/%s.json", srv_basedir, app_id);
 145            utimes(afn, NULL);
 146        }
 147    }
 148
 149    return token;
 150}
 151
 152
 153int token_del(const char *id)
 154/* deletes a token */
 155{
 156    if (!xs_is_hex(id))
 157        return -1;
 158
 159    xs *fn = xs_fmt("%s/token/%s.json", srv_basedir, id);
 160
 161    return unlink(fn);
 162}
 163
 164
 165const char * const login_page = ""
 166"<!DOCTYPE html>\n"
 167"<html>\n"
 168"<head>\n"
 169"<title>%s OAuth - Snac2</title>\n"
 170"<meta content=\"width=device-width, initial-scale=1, minimum-scale=1, user-scalable=no\" name=\"viewport\">"
 171"<style>:root {color-scheme: light dark}</style>\n"
 172"</head>\n"
 173"<body><h1>%s OAuth identify</h1>\n"
 174"<div style=\"background-color: red; color: white\">%s</div>\n"
 175"<form method=\"post\" action=\"%s:/" "/%s/%s\">\n"
 176"<p>Login: <input type=\"text\" name=\"login\" autocapitalize=\"off\"></p>\n"
 177"<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
 178"<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
 179"<input type=\"hidden\" name=\"cid\" value=\"%s\">\n"
 180"<input type=\"hidden\" name=\"state\" value=\"%s\">\n"
 181"<input type=\"submit\" value=\"OK\">\n"
 182"</form><p>%s</p></body></html>\n"
 183"";
 184
 185int oauth_get_handler(const xs_dict *req, const char *q_path,
 186                      char **body, int *b_size, char **ctype)
 187{
 188    (void)b_size;
 189
 190    if (!xs_startswith(q_path, "/oauth/"))
 191        return 0;
 192
 193    int status   = HTTP_STATUS_NOT_FOUND;
 194    const xs_dict *msg = xs_dict_get(req, "q_vars");
 195    xs *cmd      = xs_replace_n(q_path, "/oauth", "", 1);
 196
 197    srv_debug(1, xs_fmt("oauth_get_handler %s", q_path));
 198
 199    if (strcmp(cmd, "/authorize") == 0) { /** **/
 200        const char *cid   = xs_dict_get(msg, "client_id");
 201        const char *ruri  = xs_dict_get(msg, "redirect_uri");
 202        const char *rtype = xs_dict_get(msg, "response_type");
 203        const char *state = xs_dict_get(msg, "state");
 204
 205        status = HTTP_STATUS_BAD_REQUEST;
 206
 207        if (cid && ruri && rtype && strcmp(rtype, "code") == 0) {
 208            xs *app = app_get(cid);
 209
 210            if (app != NULL) {
 211                const char *host = xs_dict_get(srv_config, "host");
 212                const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 213
 214                if (xs_is_null(state))
 215                    state = "";
 216
 217                *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-login",
 218                                ruri, cid, state, USER_AGENT);
 219                *ctype = "text/html";
 220                status = HTTP_STATUS_OK;
 221
 222                srv_debug(1, xs_fmt("oauth authorize: generating login page"));
 223            }
 224            else
 225                srv_debug(1, xs_fmt("oauth authorize: bad client_id %s", cid));
 226        }
 227        else
 228            srv_debug(1, xs_fmt("oauth authorize: invalid or unset arguments"));
 229    }
 230    else
 231    if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
 232        const char *host = xs_dict_get(srv_config, "host");
 233        const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 234
 235        *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-get-token",
 236                        "", "", "", USER_AGENT);
 237        *ctype = "text/html";
 238        status = HTTP_STATUS_OK;
 239
 240    }
 241
 242    return status;
 243}
 244
 245
 246int oauth_post_handler(const xs_dict *req, const char *q_path,
 247                       const char *payload, int p_size,
 248                       char **body, int *b_size, char **ctype)
 249{
 250    (void)p_size;
 251    (void)b_size;
 252
 253    if (!xs_startswith(q_path, "/oauth/"))
 254        return 0;
 255
 256    int status   = HTTP_STATUS_NOT_FOUND;
 257
 258    const char *i_ctype = xs_dict_get(req, "content-type");
 259    xs *args      = NULL;
 260
 261    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
 262        if (!xs_is_null(payload))
 263            args = xs_json_loads(payload);
 264    }
 265    else
 266    if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded") && payload) {
 267        args    = xs_url_vars(payload);
 268    }
 269    else
 270        args = xs_dup(xs_dict_get(req, "p_vars"));
 271
 272    if (args == NULL)
 273        return HTTP_STATUS_BAD_REQUEST;
 274
 275    xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
 276
 277    srv_debug(1, xs_fmt("oauth_post_handler %s", q_path));
 278
 279    if (strcmp(cmd, "/x-snac-login") == 0) { /** **/
 280        const char *login  = xs_dict_get(args, "login");
 281        const char *passwd = xs_dict_get(args, "passwd");
 282        const char *redir  = xs_dict_get(args, "redir");
 283        const char *cid    = xs_dict_get(args, "cid");
 284        const char *state  = xs_dict_get(args, "state");
 285        const char *host   = xs_dict_get(srv_config, "host");
 286        const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 287
 288        /* by default, generate another login form with an error */
 289        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-login",
 290                        redir, cid, state, USER_AGENT);
 291        *ctype = "text/html";
 292        status = HTTP_STATUS_OK;
 293
 294        if (login && passwd && redir && cid) {
 295            snac snac;
 296
 297            if (user_open(&snac, login)) {
 298                const char *addr = xs_or(xs_dict_get(req, "remote-addr"),
 299                                         xs_dict_get(req, "x-forwarded-for"));
 300
 301                if (badlogin_check(login, addr)) {
 302                    /* check the login + password */
 303                    if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
 304                        /* success! redirect to the desired uri */
 305                        xs *code = random_str();
 306
 307                        xs_free(*body);
 308
 309                        if (strcmp(redir, "urn:ietf:wg:oauth:2.0:oob") == 0) {
 310                            *body = xs_dup(code);
 311                        }
 312                        else {
 313                            if (xs_str_in(redir, "?") != -1)
 314                                *body = xs_fmt("%s&code=%s", redir, code);
 315                            else
 316                                *body = xs_fmt("%s?code=%s", redir, code);
 317
 318                            status = HTTP_STATUS_SEE_OTHER;
 319                        }
 320
 321                        /* if there is a state, add it */
 322                        if (!xs_is_null(state) && *state) {
 323                            *body = xs_str_cat(*body, "&state=");
 324                            *body = xs_str_cat(*body, state);
 325                        }
 326
 327                        srv_log(xs_fmt("oauth x-snac-login: '%s' success, redirect to %s",
 328                                   login, *body));
 329
 330                        /* assign the login to the app */
 331                        xs *app = app_get(cid);
 332
 333                        if (app != NULL) {
 334                            app = xs_dict_set(app, "uid",  login);
 335                            app = xs_dict_set(app, "code", code);
 336                            app_add(cid, app);
 337                        }
 338                        else
 339                            srv_log(xs_fmt("oauth x-snac-login: error getting app %s", cid));
 340                    }
 341                    else {
 342                        srv_debug(1, xs_fmt("oauth x-snac-login: login '%s' incorrect", login));
 343                        badlogin_inc(login, addr);
 344                    }
 345                }
 346
 347                user_free(&snac);
 348            }
 349            else
 350                srv_debug(1, xs_fmt("oauth x-snac-login: bad user '%s'", login));
 351        }
 352        else
 353            srv_debug(1, xs_fmt("oauth x-snac-login: invalid or unset arguments"));
 354    }
 355    else
 356    if (strcmp(cmd, "/token") == 0) { /** **/
 357        xs *wrk = NULL;
 358        const char *gtype = xs_dict_get(args, "grant_type");
 359        const char *code  = xs_dict_get(args, "code");
 360        const char *cid   = xs_dict_get(args, "client_id");
 361        const char *csec  = xs_dict_get(args, "client_secret");
 362        const char *ruri  = xs_dict_get(args, "redirect_uri");
 363        const char *scope = xs_dict_get(args, "scope");
 364
 365        /* no client_secret? check if it's inside an authorization header
 366           (AndStatus does it this way) */
 367        if (xs_is_null(csec)) {
 368            const char *auhdr = xs_dict_get(req, "authorization");
 369
 370            if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) {
 371                xs *s1 = xs_replace_n(auhdr, "Basic ", "", 1);
 372                int size;
 373                xs *s2 = xs_base64_dec(s1, &size);
 374
 375                if (!xs_is_null(s2)) {
 376                    xs *l1 = xs_split(s2, ":");
 377
 378                    if (xs_list_len(l1) == 2) {
 379                        wrk = xs_dup(xs_list_get(l1, 1));
 380                        csec = wrk;
 381                    }
 382                }
 383            }
 384        }
 385
 386        /* no code?
 387           I'm not sure of the impacts of this right now, but Subway Tooter does not
 388           provide a code so one must be generated */
 389        if (xs_is_null(code)){
 390            code = random_str();
 391        }
 392        if (gtype && code && cid && csec && ruri) {
 393            xs *app = app_get(cid);
 394
 395            if (app == NULL) {
 396                status = HTTP_STATUS_UNAUTHORIZED;
 397                srv_log(xs_fmt("oauth token: invalid app %s", cid));
 398            }
 399            else
 400            if (strcmp(csec, xs_dict_get(app, "client_secret")) != 0) {
 401                status = HTTP_STATUS_UNAUTHORIZED;
 402                srv_log(xs_fmt("oauth token: invalid client_secret for app %s", cid));
 403            }
 404            else {
 405                xs *rsp   = xs_dict_new();
 406                xs *cat   = xs_number_new(time(NULL));
 407                xs *tokid = random_str();
 408
 409                rsp = xs_dict_append(rsp, "access_token", tokid);
 410                rsp = xs_dict_append(rsp, "token_type",   "Bearer");
 411                rsp = xs_dict_append(rsp, "created_at",   cat);
 412
 413                if (!xs_is_null(scope))
 414                    rsp = xs_dict_append(rsp, "scope", scope);
 415
 416                *body  = xs_json_dumps(rsp, 4);
 417                *ctype = "application/json";
 418                status = HTTP_STATUS_OK;
 419
 420                const char *uid = xs_dict_get(app, "uid");
 421
 422                srv_debug(1, xs_fmt("oauth token: "
 423                                "successful login for %s, new token %s", uid, tokid));
 424
 425                xs *token = xs_dict_new();
 426                token = xs_dict_append(token, "token",         tokid);
 427                token = xs_dict_append(token, "client_id",     cid);
 428                token = xs_dict_append(token, "client_secret", csec);
 429                token = xs_dict_append(token, "uid",           uid);
 430                token = xs_dict_append(token, "code",          code);
 431
 432                token_add(tokid, token);
 433            }
 434        }
 435        else {
 436            srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
 437            status = HTTP_STATUS_BAD_REQUEST;
 438        }
 439    }
 440    else
 441    if (strcmp(cmd, "/revoke") == 0) { /** **/
 442        const char *cid   = xs_dict_get(args, "client_id");
 443        const char *csec  = xs_dict_get(args, "client_secret");
 444        const char *tokid = xs_dict_get(args, "token");
 445
 446        if (cid && csec && tokid) {
 447            xs *token = token_get(tokid);
 448
 449            *body  = xs_str_new("{}");
 450            *ctype = "application/json";
 451
 452            if (token == NULL || strcmp(csec, xs_dict_get(token, "client_secret")) != 0) {
 453                srv_debug(1, xs_fmt("oauth revoke: bad secret for token %s", tokid));
 454                status = HTTP_STATUS_FORBIDDEN;
 455            }
 456            else {
 457                token_del(tokid);
 458                srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
 459                status = HTTP_STATUS_OK;
 460
 461                /* also delete the app, as it serves no purpose from now on */
 462                app_del(cid);
 463            }
 464        }
 465        else {
 466            srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
 467            status = HTTP_STATUS_FORBIDDEN;
 468        }
 469    }
 470    if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
 471        const char *login  = xs_dict_get(args, "login");
 472        const char *passwd = xs_dict_get(args, "passwd");
 473        const char *host   = xs_dict_get(srv_config, "host");
 474        const char *proto  = xs_dict_get_def(srv_config, "protocol", "https");
 475
 476        /* by default, generate another login form with an error */
 477        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-get-token",
 478                        "", "", "", USER_AGENT);
 479        *ctype = "text/html";
 480        status = HTTP_STATUS_OK;
 481
 482        if (login && passwd) {
 483            snac user;
 484
 485            if (user_open(&user, login)) {
 486                const char *addr = xs_or(xs_dict_get(req, "remote-addr"),
 487                                         xs_dict_get(req, "x-forwarded-for"));
 488
 489                if (badlogin_check(login, addr)) {
 490                    /* check the login + password */
 491                    if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
 492                        /* success! create a new token */
 493                        xs *tokid = random_str();
 494
 495                        srv_debug(1, xs_fmt("x-snac-new-token: "
 496                                    "successful login for %s, new token %s", login, tokid));
 497
 498                        xs *token = xs_dict_new();
 499                        token = xs_dict_append(token, "token",         tokid);
 500                        token = xs_dict_append(token, "client_id",     "snac-client");
 501                        token = xs_dict_append(token, "client_secret", "");
 502                        token = xs_dict_append(token, "uid",           login);
 503                        token = xs_dict_append(token, "code",          "");
 504
 505                        token_add(tokid, token);
 506
 507                        *ctype = "text/plain";
 508                        xs_free(*body);
 509                        *body = xs_dup(tokid);
 510                    }
 511                    else
 512                        badlogin_inc(login, addr);
 513
 514                    user_free(&user);
 515                }
 516            }
 517        }
 518    }
 519
 520    return status;
 521}
 522
 523
 524xs_str *mastoapi_id(const xs_dict *msg)
 525/* returns a somewhat Mastodon-compatible status id */
 526{
 527    const char *id = xs_dict_get(msg, "id");
 528    xs *md5        = xs_md5_hex(id, strlen(id));
 529
 530    return xs_fmt("%10.0f%s", object_ctime_by_md5(md5), md5);
 531}
 532
 533#define MID_TO_MD5(id) (id + 10)
 534
 535
 536static xs_val *get_count_from_actor(const xs_dict *actor, const char *field_names[], const char *collection_url_field)
 537/* helper to extract count from actor dict using various field name variations or from cached collection */
 538{
 539    xs_val *count = NULL;
 540
 541    /* try direct field name variations first */
 542    for (int i = 0; field_names[i] && xs_type(count) != XSTYPE_NUMBER; i++) {
 543        const xs_number *val = xs_dict_get(actor, field_names[i]);
 544        if (xs_type(val) == XSTYPE_NUMBER)
 545            count = xs_dup(val);
 546    }
 547
 548    /* if not found directly, try to get from cached collection object */
 549    if (xs_type(count) != XSTYPE_NUMBER) {
 550        const char *url = xs_dict_get(actor, collection_url_field);
 551        if (!xs_is_null(url)) {
 552            xs *coll = NULL;
 553            if (valid_status(object_get(url, &coll))) {
 554                const xs_number *total = xs_dict_get(coll, "totalItems");
 555                if (xs_type(total) == XSTYPE_NUMBER)
 556                    count = xs_dup(total);
 557            }
 558        }
 559    }
 560
 561    return count;
 562}
 563
 564
 565static const xs_list *get_collection_items(snac *snac, const char *collection_url,
 566                                           xs_dict **out_collection, xs_dict **out_page)
 567/* fetches items from an ActivityPub collection (outbox, etc) with minimal HTTP requests */
 568{
 569    const xs_list *items = NULL;
 570    xs_dict *collection = NULL;
 571
 572    if (valid_status(activitypub_request(snac, collection_url, &collection))) {
 573        /* check if items are directly embedded */
 574        items = xs_dict_get(collection, "orderedItems");
 575        if (xs_is_null(items))
 576            items = xs_dict_get(collection, "items");
 577
 578        if (!xs_is_null(items)) {
 579            /* items found in main collection - transfer ownership to keep items valid */
 580            if (out_collection)
 581                *out_collection = collection;
 582            return items;
 583        }
 584
 585        /* if no items, try fetching first page (only 1 extra request) */
 586        const char *first_url = xs_dict_get(collection, "first");
 587        if (!xs_is_null(first_url)) {
 588            xs_dict *first_page = NULL;
 589            if (valid_status(activitypub_request(snac, first_url, &first_page))) {
 590                items = xs_dict_get(first_page, "orderedItems");
 591                if (xs_is_null(items))
 592                    items = xs_dict_get(first_page, "items");
 593
 594                if (!xs_is_null(items)) {
 595                    /* items found in first page - transfer ownership to keep items valid */
 596                    if (out_page)
 597                        *out_page = first_page;
 598                    xs_free(collection);
 599                    return items;
 600                }
 601                xs_free(first_page);
 602            }
 603        }
 604        xs_free(collection);
 605    }
 606
 607    return NULL;
 608}
 609
 610
 611static const xs_dict *extract_post_from_item(const xs_val *item)
 612/* extracts the post object from an outbox item, handling Create/Announce wrappers */
 613{
 614    if (xs_type(item) != XSTYPE_DICT)
 615        return NULL;
 616
 617    const char *item_type = xs_dict_get(item, "type");
 618    const xs_dict *post = item;
 619
 620    /* if it's an activity, try to get embedded object */
 621    if (!xs_is_null(item_type) &&
 622        (strcmp(item_type, "Create") == 0 || strcmp(item_type, "Announce") == 0)) {
 623        const xs_val *obj = xs_dict_get(item, "object");
 624
 625        /* only use embedded objects, skip URL references (would need HTTP fetch) */
 626        if (!xs_is_null(obj) && xs_type(obj) == XSTYPE_DICT)
 627            post = obj;
 628        else
 629            return NULL;
 630    }
 631
 632    return post;
 633}
 634
 635
 636static int is_valid_post_type(const char *post_type)
 637/* checks if a type is a valid post type for timeline display */
 638{
 639    return !xs_is_null(post_type) &&
 640           (strcmp(post_type, "Note") == 0 ||
 641            strcmp(post_type, "Article") == 0 ||
 642            strcmp(post_type, "Question") == 0 ||
 643            strcmp(post_type, "Page") == 0);
 644}
 645
 646
 647xs_dict *mastoapi_account(snac *logged, const xs_dict *actor)
 648/* converts an ActivityPub actor to a Mastodon account */
 649{
 650    const char *id  = xs_dict_get(actor, "id");
 651    const char *pub = xs_dict_get(actor, "published");
 652    const char *proxy = NULL;
 653
 654    if (xs_type(id) != XSTYPE_STRING)
 655        return NULL;
 656
 657    if (logged && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
 658        proxy = logged->actor;
 659
 660    const char *prefu = xs_dict_get(actor, "preferredUsername");
 661
 662    const char *display_name = xs_dict_get(actor, "name");
 663    if (xs_is_null(display_name) || *display_name == '\0')
 664        display_name = prefu;
 665
 666    xs_dict *acct = xs_dict_new();
 667    xs *acct_md5  = xs_md5_hex(id, strlen(id));
 668    acct = xs_dict_append(acct, "id",           acct_md5);
 669    acct = xs_dict_append(acct, "username",     prefu);
 670    acct = xs_dict_append(acct, "display_name", display_name);
 671    acct = xs_dict_append(acct, "discoverable", xs_stock(XSTYPE_TRUE));
 672    acct = xs_dict_append(acct, "group",        xs_stock(XSTYPE_FALSE));
 673    acct = xs_dict_append(acct, "hide_collections", xs_stock(XSTYPE_FALSE));
 674    acct = xs_dict_append(acct, "indexable",    xs_stock(XSTYPE_TRUE));
 675    acct = xs_dict_append(acct, "noindex",      xs_stock(XSTYPE_FALSE));
 676    acct = xs_dict_append(acct, "roles",        xs_stock(XSTYPE_LIST));
 677
 678    {
 679        /* create the acct field as user@host */
 680        xs *l     = xs_split(id, "/");
 681        xs *fquid = xs_fmt("%s@%s", prefu, xs_list_get(l, 2));
 682        acct      = xs_dict_append(acct, "acct", fquid);
 683    }
 684
 685    if (pub)
 686        acct = xs_dict_append(acct, "created_at", pub);
 687    else {
 688        /* unset created_at crashes Tusky, so lie like a mf */
 689        xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
 690        acct = xs_dict_append(acct, "created_at", date);
 691    }
 692
 693    xs *last_status_at = xs_str_utctime(0, "%Y-%m-%d");
 694    acct = xs_dict_append(acct, "last_status_at", last_status_at);
 695
 696    const char *note = xs_dict_get(actor, "summary");
 697    if (xs_is_null(note))
 698        note = "";
 699
 700    if (strcmp(xs_dict_get(actor, "type"), "Service") == 0)
 701        acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_TRUE));
 702    else
 703        acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_FALSE));
 704
 705    acct = xs_dict_append(acct, "note", note);
 706
 707    acct = xs_dict_append(acct, "url", id);
 708    acct = xs_dict_append(acct, "uri", id);
 709
 710    xs *avatar  = NULL;
 711    const xs_dict *av = xs_dict_get(actor, "icon");
 712
 713    if (xs_type(av) == XSTYPE_DICT) {
 714        const char *url = xs_dict_get(av, "url");
 715
 716        if (url != NULL)
 717            avatar = make_url(url, proxy, 1);
 718    }
 719
 720    if (avatar == NULL)
 721        avatar = xs_fmt("%s/susie.png", srv_baseurl);
 722
 723    acct = xs_dict_append(acct, "avatar", avatar);
 724    acct = xs_dict_append(acct, "avatar_static", avatar);
 725
 726    xs *header  = NULL;
 727    const xs_dict *hd = xs_dict_get(actor, "image");
 728
 729    if (xs_type(hd) == XSTYPE_DICT)
 730        header = make_url(xs_dict_get(hd, "url"), proxy, 1);
 731
 732    if (xs_is_null(header))
 733        header = xs_str_new(NULL);
 734
 735    acct = xs_dict_append(acct, "header", header);
 736    acct = xs_dict_append(acct, "header_static", header);
 737
 738    /* emojis */
 739    const xs_list *p;
 740    if (!xs_is_null(p = xs_dict_get(actor, "tag"))) {
 741        xs *eml = xs_list_new();
 742        const xs_dict *v;
 743        int c = 0;
 744
 745        while (xs_list_next(p, &v, &c)) {
 746            const char *type = xs_dict_get(v, "type");
 747
 748            if (!xs_is_null(type) && strcmp(type, "Emoji") == 0) {
 749                const char *name    = xs_dict_get(v, "name");
 750                const xs_dict *icon = xs_dict_get(v, "icon");
 751
 752                if (!xs_is_null(name) && !xs_is_null(icon)) {
 753                    const char *o_url = xs_dict_get(icon, "url");
 754
 755                    if (!xs_is_null(o_url)) {
 756                        xs *url = make_url(o_url, proxy, 1);
 757                        xs *nm = xs_strip_chars_i(xs_dup(name), ":");
 758                        xs *d1 = xs_dict_new();
 759
 760                        d1 = xs_dict_append(d1, "shortcode",         nm);
 761                        d1 = xs_dict_append(d1, "url",               url);
 762                        d1 = xs_dict_append(d1, "static_url",        url);
 763                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
 764
 765                        eml = xs_list_append(eml, d1);
 766                    }
 767                }
 768            }
 769        }
 770
 771        acct = xs_dict_append(acct, "emojis", eml);
 772    }
 773
 774    acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
 775
 776    /* try to get counts from actor object if available (some servers include these) */
 777    const char *fcount_fields[] = { "followersCount", "followers_count", NULL };
 778    const char *gcount_fields[] = { "followingCount", "following_count", NULL };
 779    const char *scount_fields[] = { "statusesCount", "statuses_count", "totalItems", NULL };
 780
 781    xs *followers_count = get_count_from_actor(actor, fcount_fields, "followers");
 782    xs *following_count = get_count_from_actor(actor, gcount_fields, "following");
 783    xs *statuses_count = get_count_from_actor(actor, scount_fields, "outbox");
 784
 785    acct = xs_dict_append(acct, "followers_count",
 786        xs_type(followers_count) == XSTYPE_NUMBER ? followers_count : xs_stock(0));
 787    acct = xs_dict_append(acct, "following_count",
 788        xs_type(following_count) == XSTYPE_NUMBER ? following_count : xs_stock(0));
 789    acct = xs_dict_append(acct, "statuses_count",
 790        xs_type(statuses_count) == XSTYPE_NUMBER ? statuses_count : xs_stock(0));
 791
 792    xs *fields = xs_list_new();
 793    p = xs_dict_get(actor, "attachment");
 794    const xs_dict *v;
 795
 796    /* dict of validated links */
 797    xs_dict *val_links = NULL;
 798    const xs_dict *metadata  = xs_stock(XSTYPE_DICT);
 799    snac user = {0};
 800
 801    if (xs_startswith(id, srv_baseurl)) {
 802        /* if it's a local user, open it and pick its validated links */
 803        if (user_open(&user, prefu)) {
 804            val_links = user.links;
 805            metadata  = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT));
 806
 807            /* does this user want to publish their contact metrics? */
 808            if (xs_is_true(xs_dict_get(user.config, "show_contact_metrics"))) {
 809                int fwing = following_list_len(&user);
 810                int fwers = follower_list_len(&user);
 811                xs *ni = xs_number_new(fwing);
 812                xs *ne = xs_number_new(fwers);
 813
 814                acct = xs_dict_append(acct, "followers_count", ne);
 815                acct = xs_dict_append(acct, "following_count", ni);
 816            }
 817        }
 818    }
 819
 820    if (xs_is_null(val_links))
 821        val_links = xs_stock(XSTYPE_DICT);
 822
 823    int c = 0;
 824    while (xs_list_next(p, &v, &c)) {
 825        const char *type  = xs_dict_get(v, "type");
 826        const char *name  = xs_dict_get(v, "name");
 827        const char *value = xs_dict_get(v, "value");
 828
 829        if (!xs_is_null(type) && !xs_is_null(name) &&
 830            !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) {
 831            xs *val_date = NULL;
 832
 833            const char *url = xs_dict_get(metadata, name);
 834
 835            if (!xs_is_null(url) && xs_startswith(url, "https:/" "/")) {
 836                const xs_number *verified_time = xs_dict_get(val_links, url);
 837                if (xs_type(verified_time) == XSTYPE_NUMBER) {
 838                    time_t t = xs_number_get(verified_time);
 839
 840                    if (t > 0)
 841                        val_date = xs_str_utctime(t, ISO_DATE_SPEC);
 842                }
 843            }
 844
 845            xs *d = xs_dict_new();
 846
 847            d = xs_dict_append(d, "name", name);
 848            d = xs_dict_append(d, "value", value);
 849            d = xs_dict_append(d, "verified_at",
 850                xs_type(val_date) == XSTYPE_STRING && *val_date ?
 851                    val_date : xs_stock(XSTYPE_NULL));
 852
 853            fields = xs_list_append(fields, d);
 854        }
 855    }
 856
 857    user_free(&user);
 858
 859    acct = xs_dict_append(acct, "fields", fields);
 860
 861    return acct;
 862}
 863
 864
 865xs_str *mastoapi_date(const char *date)
 866/* converts an ISO 8601 date to whatever format Mastodon uses */
 867{
 868    xs_str *s = xs_crop_i(xs_dup(date), 0, 19);
 869    s = xs_str_cat(s, ".000Z");
 870
 871    return s;
 872}
 873
 874
 875xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg)
 876/* creates a mastoapi Poll object */
 877{
 878    xs_dict *poll = xs_dict_new();
 879    xs *mid       = mastoapi_id(msg);
 880    const xs_list *opts = NULL;
 881    const xs_val *v;
 882    int num_votes = 0;
 883    xs *options = xs_list_new();
 884
 885    poll = xs_dict_append(poll, "id", mid);
 886    const char *date = xs_dict_get(msg, "endTime");
 887    if (date == NULL)
 888        date = xs_dict_get(msg, "closed");
 889    if (date == NULL)
 890        return NULL;
 891
 892    xs *fd = mastoapi_date(date);
 893    poll = xs_dict_append(poll, "expires_at", fd);
 894
 895    date = xs_dict_get(msg, "closed");
 896    time_t t = 0;
 897
 898    if (date != NULL)
 899        t = xs_parse_iso_date(date, 0);
 900
 901    poll = xs_dict_append(poll, "expired",
 902            t < time(NULL) ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE));
 903
 904    if ((opts = xs_dict_get(msg, "oneOf")) != NULL)
 905        poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_FALSE));
 906    else {
 907        opts = xs_dict_get(msg, "anyOf");
 908        poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_TRUE));
 909    }
 910
 911    int c = 0;
 912    while (xs_list_next(opts, &v, &c)) {
 913        const char *title   = xs_dict_get(v, "name");
 914        const char *replies = xs_dict_get(v, "replies");
 915
 916        if (title && replies) {
 917            const char *votes_count = xs_dict_get(replies, "totalItems");
 918
 919            if (xs_type(votes_count) == XSTYPE_NUMBER) {
 920                xs *d = xs_dict_new();
 921                d = xs_dict_append(d, "title",  title);
 922                d = xs_dict_append(d, "votes_count", votes_count);
 923
 924                options = xs_list_append(options, d);
 925                num_votes += xs_number_get(votes_count);
 926            }
 927        }
 928    }
 929
 930    poll = xs_dict_append(poll, "options", options);
 931    xs *vc = xs_number_new(num_votes);
 932    poll = xs_dict_append(poll, "votes_count", vc);
 933
 934    poll = xs_dict_append(poll, "emojis", xs_stock(XSTYPE_LIST));
 935
 936    poll = xs_dict_append(poll, "voted",
 937            (snac && was_question_voted(snac, xs_dict_get(msg, "id"))) ?
 938                xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
 939
 940    return poll;
 941}
 942
 943
 944xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
 945/* converts an ActivityPub note to a Mastodon status */
 946{
 947    xs *actor = NULL;
 948    actor_get_refresh(snac, get_atto(msg), &actor);
 949    const char *proxy = NULL;
 950
 951    /* if the author is not here, discard */
 952    if (actor == NULL)
 953        return NULL;
 954
 955    if (snac && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
 956        proxy = snac->actor;
 957
 958    const char *type = xs_dict_get(msg, "type");
 959    const char *id   = xs_dict_get(msg, "id");
 960
 961    /* fail if it's not a valid actor */
 962    if (xs_is_null(type) || xs_is_null(id))
 963        return NULL;
 964
 965    xs *acct = mastoapi_account(snac, actor);
 966    if (acct == NULL)
 967        return NULL;
 968
 969    xs *idx = NULL;
 970    xs *ixc = NULL;
 971    const char *tmp;
 972    xs *mid  = mastoapi_id(msg);
 973
 974    xs_dict *st = xs_dict_new();
 975
 976    st = xs_dict_append(st, "id",           mid);
 977    st = xs_dict_append(st, "uri",          id);
 978    st = xs_dict_append(st, "url",          id);
 979    st = xs_dict_append(st, "account",      acct);
 980
 981    const char *published = xs_dict_get(msg, "published");
 982    xs *fd = NULL;
 983
 984    if (published)
 985        fd = mastoapi_date(published);
 986    else {
 987        xs *p = xs_str_iso_date(0);
 988        fd = mastoapi_date(p);
 989    }
 990
 991    st = xs_dict_append(st, "created_at", fd);
 992
 993    {
 994        const char *content = xs_dict_get(msg, "content");
 995        const char *name    = xs_dict_get(msg, "name");
 996        xs *s1 = NULL;
 997
 998        if (name && content)
 999            s1 = xs_fmt("%s<br><br>%s", name, content);
1000        else
1001        if (name)
1002            s1 = xs_dup(name);
1003        else
1004        if (content)
1005            s1 = xs_dup(content);
1006        else
1007            s1 = xs_str_new(NULL);
1008
1009        st = xs_dict_append(st, "content", s1);
1010    }
1011
1012    /* determine visibility: https://docs.joinmastodon.org/entities/Status/#visibility */
1013    const int scope = get_msg_visibility(msg);
1014    if (scope == SCOPE_PUBLIC)
1015        st = xs_dict_append(st, "visibility", "public");
1016    else if (scope == SCOPE_FOLLOWERS)
1017        st = xs_dict_append(st, "visibility", "private");
1018    else if (scope == SCOPE_MENTIONED)
1019        st = xs_dict_append(st, "visibility", "direct");
1020    else if (scope == SCOPE_UNLISTED)
1021        st = xs_dict_append(st, "visibility", "unlisted");
1022
1023    tmp = xs_dict_get(msg, "sensitive");
1024    if (xs_is_null(tmp))
1025        tmp = xs_stock(XSTYPE_FALSE);
1026
1027    st = xs_dict_append(st, "sensitive",    tmp);
1028
1029    tmp = xs_dict_get(msg, "summary");
1030    if (xs_is_null(tmp))
1031        tmp = "";
1032
1033    st = xs_dict_append(st, "spoiler_text", tmp);
1034
1035    /* create the list of attachments */
1036    xs *attach = get_attachments(msg);
1037
1038    {
1039        xs_list *p = attach;
1040        const xs_dict *v;
1041
1042        xs *matt = xs_list_new();
1043
1044        while (xs_list_iter(&p, &v)) {
1045            const char *type = xs_dict_get(v, "type");
1046            const char *o_href = xs_dict_get(v, "href");
1047            const char *name = xs_dict_get(v, "name");
1048
1049            if (xs_match(type, "image/*|video/*|audio/*|Image|Video")) { /* */
1050                xs *matteid = xs_fmt("%s_%d", id, xs_list_len(matt));
1051                xs *href = make_url(o_href, proxy, 1);
1052
1053                xs *d = xs_dict_new();
1054
1055                d = xs_dict_append(d, "id",          matteid);
1056                d = xs_dict_append(d, "url",         href);
1057                d = xs_dict_append(d, "preview_url", href);
1058                d = xs_dict_append(d, "remote_url",  href);
1059                d = xs_dict_append(d, "description", name);
1060
1061                d = xs_dict_append(d, "type", (*type == 'v' || *type == 'V') ? "video" :
1062                                              (*type == 'a' || *type == 'A') ? "audio" : "image");
1063
1064                matt = xs_list_append(matt, d);
1065            }
1066        }
1067
1068        st = xs_dict_append(st, "media_attachments", matt);
1069    }
1070
1071    {
1072        xs *ml  = xs_list_new();
1073        xs *htl = xs_list_new();
1074        xs *eml = xs_list_new();
1075        const xs_list *tag = xs_dict_get(msg, "tag");
1076        int n = 0;
1077
1078        xs *tag_list = NULL;
1079
1080        if (xs_type(tag) == XSTYPE_DICT) {
1081            tag_list = xs_list_new();
1082            tag_list = xs_list_append(tag_list, tag);
1083        }
1084        else
1085        if (xs_type(tag) == XSTYPE_LIST)
1086            tag_list = xs_dup(tag);
1087        else
1088            tag_list = xs_list_new();
1089
1090        tag = tag_list;
1091        const xs_dict *v;
1092
1093        int c = 0;
1094        while (xs_list_next(tag, &v, &c)) {
1095            const char *type = xs_dict_get(v, "type");
1096
1097            if (xs_is_null(type))
1098                continue;
1099
1100            xs *d1 = xs_dict_new();
1101
1102            if (strcmp(type, "Mention") == 0) {
1103                const char *name = xs_dict_get(v, "name");
1104                const char *href = xs_dict_get(v, "href");
1105
1106                if (!xs_is_null(name) && !xs_is_null(href) &&
1107                    (snac == NULL || strcmp(href, snac->actor) != 0)) {
1108                    xs *nm = xs_strip_chars_i(xs_dup(name), "@");
1109
1110                    xs *id = xs_fmt("%d", n++);
1111                    d1 = xs_dict_append(d1, "id", id);
1112                    d1 = xs_dict_append(d1, "username", nm);
1113                    d1 = xs_dict_append(d1, "acct", nm);
1114                    d1 = xs_dict_append(d1, "url", href);
1115
1116                    ml = xs_list_append(ml, d1);
1117                }
1118            }
1119            else
1120            if (strcmp(type, "Hashtag") == 0) {
1121                const char *name = xs_dict_get(v, "name");
1122                const char *href = xs_dict_get(v, "href");
1123
1124                if (!xs_is_null(name) && !xs_is_null(href)) {
1125                    xs *nm = xs_strip_chars_i(xs_dup(name), "#");
1126
1127                    d1 = xs_dict_append(d1, "name", nm);
1128                    d1 = xs_dict_append(d1, "url", href);
1129
1130                    htl = xs_list_append(htl, d1);
1131                }
1132            }
1133            else
1134            if (strcmp(type, "Emoji") == 0) {
1135                const char *name    = xs_dict_get(v, "name");
1136                const xs_dict *icon = xs_dict_get(v, "icon");
1137
1138                if (!xs_is_null(name) && !xs_is_null(icon)) {
1139                    const char *o_url = xs_dict_get(icon, "url");
1140
1141                    if (!xs_is_null(o_url)) {
1142                        xs *url = make_url(o_url, snac ? snac->actor : NULL, 1);
1143                        xs *nm = xs_strip_chars_i(xs_dup(name), ":");
1144
1145                        d1 = xs_dict_append(d1, "shortcode", nm);
1146                        d1 = xs_dict_append(d1, "url", url);
1147                        d1 = xs_dict_append(d1, "static_url", url);
1148                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
1149                        d1 = xs_dict_append(d1, "category", "Emojis");
1150
1151                        eml = xs_list_append(eml, d1);
1152                    }
1153                }
1154            }
1155        }
1156
1157        st = xs_dict_append(st, "mentions", ml);
1158        st = xs_dict_append(st, "tags",     htl);
1159        st = xs_dict_append(st, "emojis",   eml);
1160    }
1161
1162    xs_free(idx);
1163    xs_free(ixc);
1164    idx = object_likes(id);
1165    ixc = xs_number_new(xs_list_len(idx));
1166
1167    st = xs_dict_append(st, "favourites_count", ixc);
1168    st = xs_dict_append(st, "favourited",
1169        (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1170
1171    xs_free(idx);
1172    xs_free(ixc);
1173    idx = object_announces(id);
1174    ixc = xs_number_new(xs_list_len(idx));
1175
1176    st = xs_dict_append(st, "reblogs_count", ixc);
1177    st = xs_dict_append(st, "reblogged",
1178        (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1179
1180    /* get the last person who boosted this */
1181    xs *boosted_by_md5 = NULL;
1182    if (xs_list_len(idx))
1183        boosted_by_md5 = xs_dup(xs_list_get(idx, -1));
1184
1185    xs_free(idx);
1186    xs_free(ixc);
1187    idx = object_children(id);
1188    ixc = xs_number_new(xs_list_len(idx));
1189
1190    st = xs_dict_append(st, "replies_count", ixc);
1191
1192    /* default in_reply_to values */
1193    st = xs_dict_append(st, "in_reply_to_id",         xs_stock(XSTYPE_NULL));
1194    st = xs_dict_append(st, "in_reply_to_account_id", xs_stock(XSTYPE_NULL));
1195
1196    tmp = get_in_reply_to(msg);
1197    if (!xs_is_null(tmp)) {
1198        xs *irto = NULL;
1199
1200        if (valid_status(object_get(tmp, &irto))) {
1201            xs *irt_mid = mastoapi_id(irto);
1202            st = xs_dict_set(st, "in_reply_to_id", irt_mid);
1203
1204            const char *at = NULL;
1205            if (!xs_is_null(at = get_atto(irto))) {
1206                xs *at_md5 = xs_md5_hex(at, strlen(at));
1207                st = xs_dict_set(st, "in_reply_to_account_id", at_md5);
1208            }
1209        }
1210    }
1211
1212    st = xs_dict_append(st, "reblog",   xs_stock(XSTYPE_NULL));
1213    st = xs_dict_append(st, "card",     xs_stock(XSTYPE_NULL));
1214    st = xs_dict_append(st, "language", "en");
1215
1216    st = xs_dict_append(st, "filtered", xs_stock(XSTYPE_LIST));
1217    st = xs_dict_append(st, "muted",    xs_stock(XSTYPE_FALSE));
1218
1219    tmp = xs_dict_get(msg, "sourceContent");
1220    if (xs_is_null(tmp))
1221        tmp = "";
1222
1223    st = xs_dict_append(st, "text", tmp);
1224
1225    tmp = xs_dict_get(msg, "updated");
1226    xs *fd2 = NULL;
1227    if (xs_is_null(tmp))
1228        tmp = xs_stock(XSTYPE_NULL);
1229    else {
1230        fd2 = mastoapi_date(tmp);
1231        tmp = fd2;
1232    }
1233
1234    st = xs_dict_append(st, "edited_at", tmp);
1235
1236    if (strcmp(type, "Question") == 0) {
1237        xs *poll = mastoapi_poll(snac, msg);
1238        st = xs_dict_append(st, "poll", poll);
1239    }
1240    else
1241        st = xs_dict_append(st, "poll", xs_stock(XSTYPE_NULL));
1242
1243    st = xs_dict_append(st, "bookmarked",
1244        (snac && is_bookmarked(snac, id)) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1245
1246    st = xs_dict_append(st, "pinned",
1247        (snac && is_pinned(snac, id)) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1248
1249    /* is it a boost? */
1250    if (!xs_is_null(boosted_by_md5)) {
1251        /* create a new dummy status, using st as the 'reblog' field */
1252        xs_dict *bst = xs_dup(st);
1253        xs *b_actor = NULL;
1254
1255        if (valid_status(object_get_by_md5(boosted_by_md5, &b_actor))) {
1256            xs *b_acct   = mastoapi_account(snac, b_actor);
1257            xs *fake_uri = NULL;
1258
1259            if (snac)
1260                fake_uri = xs_fmt("%s/d/%s/Announce", snac->actor, mid);
1261            else
1262                fake_uri = xs_fmt("%s#%s", srv_baseurl, mid);
1263
1264            bst = xs_dict_set(bst, "uri", fake_uri);
1265            bst = xs_dict_set(bst, "url", fake_uri);
1266            bst = xs_dict_set(bst, "account", b_acct);
1267            bst = xs_dict_set(bst, "content", "");
1268            bst = xs_dict_set(bst, "reblog", st);
1269
1270            xs *b_id = xs_toupper_i(xs_dup(xs_dict_get(st, "id")));
1271            bst = xs_dict_set(bst, "id", b_id);
1272
1273            xs_free(st);
1274            st = bst;
1275        }
1276        else
1277            xs_free(bst);
1278    }
1279
1280    return st;
1281}
1282
1283
1284xs_dict *mastoapi_relationship(snac *snac, const char *md5)
1285{
1286    xs_dict *rel = NULL;
1287    xs *actor_o  = NULL;
1288
1289    if (valid_status(object_get_by_md5(md5, &actor_o))) {
1290        rel = xs_dict_new();
1291
1292        const char *actor = xs_dict_get(actor_o, "id");
1293
1294        rel = xs_dict_append(rel, "id",                   md5);
1295        rel = xs_dict_append(rel, "following",
1296            following_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1297
1298        rel = xs_dict_append(rel, "showing_reblogs",      xs_stock(XSTYPE_TRUE));
1299        rel = xs_dict_append(rel, "notifying",            xs_stock(XSTYPE_FALSE));
1300        rel = xs_dict_append(rel, "followed_by",
1301            follower_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1302
1303        rel = xs_dict_append(rel, "blocking",
1304            is_muted(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1305
1306        rel = xs_dict_append(rel, "muting",               xs_stock(XSTYPE_FALSE));
1307        rel = xs_dict_append(rel, "muting_notifications", xs_stock(XSTYPE_FALSE));
1308        rel = xs_dict_append(rel, "requested",            xs_stock(XSTYPE_FALSE));
1309        rel = xs_dict_append(rel, "domain_blocking",      xs_stock(XSTYPE_FALSE));
1310        rel = xs_dict_append(rel, "endorsed",             xs_stock(XSTYPE_FALSE));
1311        rel = xs_dict_append(rel, "note",                 "");
1312    }
1313
1314    return rel;
1315}
1316
1317
1318int process_auth_token(snac *snac, const xs_dict *req)
1319/* processes an authorization token, if there is one */
1320{
1321    int logged_in = 0;
1322    const char *v;
1323
1324    /* if there is an authorization field, try to validate it */
1325    if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
1326        xs *tokid = xs_replace_n(v, "Bearer ", "", 1);
1327        xs *token = token_get(tokid);
1328
1329        if (token != NULL) {
1330            const char *uid = xs_dict_get(token, "uid");
1331
1332            if (!xs_is_null(uid) && user_open(snac, uid)) {
1333                logged_in = 1;
1334
1335                /* this counts as a 'login' */
1336                lastlog_write(snac, "mastoapi");
1337
1338                srv_debug(2, xs_fmt("mastoapi auth: valid token for user '%s'", uid));
1339            }
1340            else
1341                srv_log(xs_fmt("mastoapi auth: corrupted token '%s'", tokid));
1342        }
1343        else
1344            srv_log(xs_fmt("mastoapi auth: invalid token '%s'", tokid));
1345    }
1346
1347    return logged_in;
1348}
1349
1350
1351void credentials_get(char **body, char **ctype, int *status, snac snac)
1352{
1353    xs *acct = xs_dict_new();
1354
1355    const xs_val *bot = xs_dict_get(snac.config, "bot");
1356
1357    acct = xs_dict_append(acct, "id", snac.md5);
1358    acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
1359    acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
1360    acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name"));
1361    acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published"));
1362    acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac.config, "published"));
1363    acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
1364    acct = xs_dict_append(acct, "url", snac.actor);
1365
1366    acct = xs_dict_append(acct, "locked",
1367        xs_stock(xs_is_true(xs_dict_get(snac.config, "approve_followers")) ? XSTYPE_TRUE : XSTYPE_FALSE));
1368
1369    acct = xs_dict_append(acct, "bot", xs_stock(xs_is_true(bot) ? XSTYPE_TRUE : XSTYPE_FALSE));
1370    acct = xs_dict_append(acct, "emojis", xs_stock(XSTYPE_LIST));
1371
1372    xs *src = xs_json_loads("{\"privacy\":\"public\", \"language\":\"en\","
1373        "\"follow_requests_count\": 0,"
1374        "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
1375    /* some apps take the note from the source object */
1376    src = xs_dict_set(src, "note", xs_dict_get(snac.config, "bio"));
1377    src = xs_dict_set(src, "privacy", xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE ? "private" : "public");
1378
1379    const xs_str *cw = xs_dict_get(snac.config, "cw");
1380    src = xs_dict_set(src, "sensitive",
1381        strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1382
1383    src = xs_dict_set(src, "bot", xs_stock(xs_is_true(bot) ? XSTYPE_TRUE : XSTYPE_FALSE));
1384
1385    xs *avatar = NULL;
1386    const char *av = xs_dict_get(snac.config, "avatar");
1387
1388    if (xs_is_null(av) || *av == '\0')
1389        avatar = xs_fmt("%s/susie.png", srv_baseurl);
1390    else
1391        avatar = xs_dup(av);
1392
1393    acct = xs_dict_append(acct, "avatar", avatar);
1394    acct = xs_dict_append(acct, "avatar_static", avatar);
1395
1396    xs *header = NULL;
1397    const char *hd = xs_dict_get(snac.config, "header");
1398
1399    if (!xs_is_null(hd))
1400        header = xs_dup(hd);
1401    else
1402        header = xs_str_new(NULL);
1403
1404    acct = xs_dict_append(acct, "header", header);
1405    acct = xs_dict_append(acct, "header_static", header);
1406
1407    xs *metadata = NULL;
1408    const xs_dict *md = xs_dict_get(snac.config, "metadata");
1409
1410    if (xs_is_dict(md))
1411        metadata = xs_dup(md);
1412    else
1413    if (xs_is_string(md)) {
1414        metadata = xs_dict_new();
1415        xs *l = xs_split(md, "\n");
1416        const char *ll;
1417
1418        xs_list_foreach(l, ll) {
1419            xs *kv = xs_split_n(ll, "=", 1);
1420            const char *k = xs_list_get(kv, 0);
1421            const char *v = xs_list_get(kv, 1);
1422
1423            if (k && v) {
1424                xs *kk = xs_strip_i(xs_dup(k));
1425                xs *vv = xs_strip_i(xs_dup(v));
1426                metadata = xs_dict_set(metadata, kk, vv);
1427            }
1428        }
1429    }
1430
1431    if (xs_is_dict(metadata)) {
1432        xs *fields = xs_list_new();
1433        const xs_str *k;
1434        const xs_str *v;
1435
1436        xs_dict *val_links = snac.links;
1437        if (xs_is_null(val_links))
1438            val_links = xs_stock(XSTYPE_DICT);
1439
1440        int c = 0;
1441        while (xs_dict_next(metadata, &k, &v, &c)) {
1442            xs *val_date = NULL;
1443
1444            const xs_number *verified_time = xs_dict_get(val_links, v);
1445            if (xs_type(verified_time) == XSTYPE_NUMBER) {
1446                time_t t = xs_number_get(verified_time);
1447
1448                if (t > 0)
1449                    val_date = xs_str_utctime(t, ISO_DATE_SPEC);
1450            }
1451
1452            xs *d = xs_dict_new();
1453
1454            d = xs_dict_append(d, "name", k);
1455            d = xs_dict_append(d, "value", v);
1456            d = xs_dict_append(d, "verified_at",
1457                               xs_type(val_date) == XSTYPE_STRING && *val_date ? val_date : xs_stock(XSTYPE_NULL));
1458
1459            fields = xs_list_append(fields, d);
1460        }
1461
1462        acct = xs_dict_set(acct, "fields", fields);
1463        /* some apps take the fields from the source object */
1464        src = xs_dict_set(src, "fields", fields);
1465    }
1466
1467    acct = xs_dict_append(acct, "source", src);
1468    acct = xs_dict_append(acct, "followers_count", xs_stock(0));
1469    acct = xs_dict_append(acct, "following_count", xs_stock(0));
1470    acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
1471
1472    /* does this user want to publish their contact metrics? */
1473    if (xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"))) {
1474        int fwing = following_list_len(&snac);
1475        int fwers = follower_list_len(&snac);
1476        xs *ni = xs_number_new(fwing);
1477        xs *ne = xs_number_new(fwers);
1478
1479        acct = xs_dict_append(acct, "followers_count", ne);
1480        acct = xs_dict_append(acct, "following_count", ni);
1481    }
1482
1483    *body = xs_json_dumps(acct, 4);
1484    *ctype = "application/json";
1485    *status = HTTP_STATUS_OK;
1486}
1487
1488
1489xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn)
1490{
1491    xs_list *out = xs_list_new();
1492    FILE *f;
1493    char md5[MD5_HEX_SIZE];
1494
1495    if (dbglevel) {
1496        xs *js = xs_json_dumps(args, 0);
1497        srv_debug(1, xs_fmt("mastoapi_timeline args %s", js));
1498    }
1499
1500    if ((f = fopen(index_fn, "r")) == NULL)
1501        return out;
1502
1503    const char *o_max_id   = xs_dict_get(args, "max_id");
1504    const char *o_since_id = xs_dict_get(args, "since_id");
1505    const char *o_min_id   = xs_dict_get(args, "min_id"); /* unsupported old-to-new navigation */
1506    const char *limit_s  = xs_dict_get(args, "limit");
1507    int (*iterator)(FILE *, char *);
1508    int initial_status = 0;
1509    int ascending = 0;
1510    int limit = 0;
1511    int cnt   = 0;
1512
1513    xs *max_id   = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL;
1514    xs *since_id = o_since_id ? xs_tolower_i(xs_dup(o_since_id)) : NULL;
1515    xs *min_id   = o_min_id ? xs_tolower_i(xs_dup(o_min_id)) : NULL;
1516
1517    if (!xs_is_null(limit_s))
1518        limit = atoi(limit_s);
1519
1520    if (limit == 0)
1521        limit = 20;
1522
1523    if (min_id) {
1524        iterator = &index_asc_next;
1525        initial_status = index_asc_first(f, md5, MID_TO_MD5(min_id));
1526        ascending = 1;
1527    }
1528    else {
1529        iterator = &index_desc_next;
1530        initial_status = index_desc_first(f, md5, 0);
1531    }
1532
1533    xs_set entries;
1534    xs_set_init(&entries);
1535
1536    if (initial_status) {
1537        do {
1538            xs *msg = NULL;
1539
1540            /* only return entries older that max_id */
1541            if (max_id) {
1542                if (strcmp(md5, MID_TO_MD5(max_id)) == 0) {
1543                    max_id = xs_free(max_id);
1544                    if (ascending)
1545                        break;
1546                }
1547                if (!ascending)
1548                    continue;
1549            }
1550
1551            /* only returns entries newer than since_id */
1552            if (since_id) {
1553                if (strcmp(md5, MID_TO_MD5(since_id)) == 0) {
1554                    if (!ascending)
1555                        break;
1556                    since_id = xs_free(since_id);
1557                }
1558                if (ascending)
1559                    continue;
1560            }
1561
1562            /* get the entry */
1563            if (user) {
1564                if (!valid_status(timeline_get_by_md5(user, md5, &msg)))
1565                    continue;
1566            }
1567            else {
1568                if (!valid_status(object_get_by_md5(md5, &msg)))
1569                    continue;
1570            }
1571
1572            /* discard non-Notes */
1573            const char *id   = xs_dict_get(msg, "id");
1574            const char *type = xs_dict_get(msg, "type");
1575            if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
1576                continue;
1577
1578            if (id && is_instance_blocked(id))
1579                continue;
1580
1581            const char *from = NULL;
1582            if (strcmp(type, "Page") == 0)
1583                from = xs_dict_get(msg, "audience");
1584
1585            if (from == NULL)
1586                from = get_atto(msg);
1587
1588            if (from == NULL)
1589                continue;
1590
1591            if (user) {
1592                /* is this message from a person we don't follow? */
1593                if (strcmp(from, user->actor) && !following_check(user, from)) {
1594                    /* discard if it was not boosted */
1595                    xs *idx = object_announces(id);
1596
1597                    if (xs_list_len(idx) == 0)
1598                        continue;
1599                }
1600
1601                /* discard notes from muted morons */
1602                if (is_muted(user, from))
1603                    continue;
1604
1605                /* discard hidden notes */
1606                if (is_hidden(user, id))
1607                    continue;
1608            }
1609            else {
1610                /* skip non-public messages */
1611                if (!is_msg_public(msg))
1612                    continue;
1613
1614                /* discard messages from private users */
1615                if (is_msg_from_private_user(msg))
1616                    continue;
1617            }
1618
1619            /* if it has a name and it's not an object that may have one,
1620               it's a poll vote, so discard it */
1621            if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video|Audio|Event"))
1622                continue;
1623
1624            /* convert the Note into a Mastodon status */
1625            xs *st = mastoapi_status(user, msg);
1626
1627            if (st != NULL && xs_set_add(&entries, md5) == 1) {
1628                if (ascending)
1629                    out = xs_list_insert(out, 0, st);
1630                else
1631                    out = xs_list_append(out, st);
1632                cnt++;
1633            }
1634
1635        } while ((cnt < limit) && (*iterator)(f, md5));
1636    }
1637
1638    xs_set_free(&entries);
1639
1640    int more = index_desc_next(f, md5);
1641
1642    fclose(f);
1643
1644    srv_debug(1, xs_fmt("mastoapi_timeline ret %d%s", cnt, more ? " (+)" : ""));
1645
1646    return out;
1647}
1648
1649
1650xs_str *timeline_link_header(const char *endpoint, xs_list *timeline)
1651/* returns a Link header with paging information */
1652{
1653    xs_str *s = NULL;
1654
1655    if (xs_list_len(timeline) == 0)
1656        return NULL;
1657
1658    const xs_dict *first_st = xs_list_get(timeline, 0);
1659    const xs_dict *last_st  = xs_list_get(timeline, -1);
1660    const char *first_id    = xs_dict_get(first_st, "id");
1661    const char *last_id     = xs_dict_get(last_st, "id");
1662    const char *host        = xs_dict_get(srv_config, "host");
1663    const char *protocol    = xs_dict_get_def(srv_config, "protocol", "https");
1664
1665    s = xs_fmt(
1666        "<%s:/" "/%s%s?max_id=%s>; rel=\"next\", "
1667        "<%s:/" "/%s%s?since_id=%s>; rel=\"prev\"",
1668        protocol, host, endpoint, last_id,
1669        protocol, host, endpoint, first_id);
1670
1671    srv_debug(1, xs_fmt("timeline_link_header %s", s));
1672
1673    return s;
1674}
1675
1676
1677xs_list *mastoapi_account_lists(snac *user, const char *uid)
1678/* returns the list of list an user is in */
1679{
1680    xs_list *out  = xs_list_new();
1681    xs *actor_md5 = NULL;
1682    xs *lol       = list_maint(user, NULL, 0);
1683
1684    if (uid) {
1685        if (!xs_is_hex(uid))
1686            actor_md5 = xs_md5_hex(uid, strlen(uid));
1687        else
1688            actor_md5 = xs_dup(uid);
1689    }
1690
1691    const xs_list *li;
1692    xs_list_foreach(lol, li) {
1693        const char *list_id    = xs_list_get(li, 0);
1694        const char *list_title = xs_list_get(li, 1);
1695        if (uid) {
1696            xs *users = list_members(user, list_id, NULL, 0);
1697            if (xs_list_in(users, actor_md5) == -1)
1698                continue;
1699        }
1700
1701        xs *d = xs_dict_new();
1702
1703        d = xs_dict_append(d, "id", list_id);
1704        d = xs_dict_append(d, "title", list_title);
1705        d = xs_dict_append(d, "replies_policy", "list");
1706        d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
1707
1708        out = xs_list_append(out, d);
1709    }
1710
1711    return out;
1712}
1713
1714
1715int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1716                         char **body, int *b_size, char **ctype, xs_str **link)
1717{
1718    (void)b_size;
1719
1720    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
1721        return 0;
1722
1723    int status    = HTTP_STATUS_NOT_FOUND;
1724    const xs_dict *args = xs_dict_get(req, "q_vars");
1725    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
1726
1727    snac snac1 = {0};
1728    int logged_in = process_auth_token(&snac1, req);
1729
1730    if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
1731        if (logged_in) {
1732            credentials_get(body, ctype, &status, snac1);
1733        }
1734        else {
1735            status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login)
1736        }
1737    }
1738    else
1739    if (strcmp(cmd, "/v1/accounts/relationships") == 0) { /** **/
1740        /* find if an account is followed, blocked, etc. */
1741        /* the account to get relationships about is in args "id[]" */
1742
1743        if (logged_in) {
1744            xs *res         = xs_list_new();
1745            const char *md5 = xs_dict_get(args, "id[]");
1746
1747            if (xs_is_null(md5))
1748                md5 = xs_dict_get(args, "id");
1749
1750            if (!xs_is_null(md5)) {
1751                if (xs_type(md5) == XSTYPE_LIST)
1752                    md5 = xs_list_get(md5, 0);
1753
1754                xs *rel = mastoapi_relationship(&snac1, md5);
1755
1756                if (rel != NULL)
1757                    res = xs_list_append(res, rel);
1758            }
1759
1760            *body  = xs_json_dumps(res, 4);
1761            *ctype = "application/json";
1762            status = HTTP_STATUS_OK;
1763        }
1764        else
1765            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
1766    }
1767    else
1768    if (strcmp(cmd, "/v1/accounts/lookup") == 0) { /** **/
1769        /* lookup an account */
1770        const char *acct = xs_dict_get(args, "acct");
1771
1772        if (!xs_is_null(acct)) {
1773            xs *s = xs_strip_chars_i(xs_dup(acct), "@");
1774            xs *l = xs_split_n(s, "@", 1);
1775            const char *uid = xs_list_get(l, 0);
1776            const char *host = xs_list_get(l, 1);
1777
1778            if (uid && (!host || strcmp(host, xs_dict_get(srv_config, "host")) == 0)) {
1779                snac user;
1780
1781                if (user_open(&user, uid)) {
1782                    xs *actor = msg_actor(&user);
1783                    xs *macct = mastoapi_account(NULL, actor);
1784
1785                    *body  = xs_json_dumps(macct, 4);
1786                    *ctype = "application/json";
1787                    status = HTTP_STATUS_OK;
1788
1789                    user_free(&user);
1790                }
1791            }
1792        }
1793    }
1794    else
1795    if (xs_startswith(cmd, "/v1/accounts/")) { /** **/
1796        /* account-related information */
1797        xs *l = xs_split(cmd, "/");
1798        const char *uid = xs_list_get(l, 3);
1799        const char *opt = xs_list_get(l, 4);
1800
1801        if (uid != NULL) {
1802            snac snac2;
1803            xs *out   = NULL;
1804            xs *actor = NULL;
1805
1806            if (logged_in && strcmp(uid, "familiar_followers") == 0) { /** **/
1807                /* familiar followers endpoint - return empty array */
1808                out = xs_list_new();
1809            }
1810            else
1811            if (logged_in && strcmp(uid, "search") == 0) { /** **/
1812                /* search for accounts starting with q */
1813                const char *aq = xs_dict_get(args, "q");
1814
1815                if (!xs_is_null(aq)) {
1816                    xs *q    = xs_utf8_to_lower(aq);
1817                    out      = xs_list_new();
1818                    xs *wing = following_list(&snac1);
1819                    xs *wers = follower_list(&snac1);
1820                    xs *ulst = user_list();
1821                    xs_list *p;
1822                    const xs_str *v;
1823                    xs_set seen;
1824
1825                    xs_set_init(&seen);
1826
1827                    /* user relations */
1828                    xs_list *lsts[] = { wing, wers, NULL };
1829                    int n;
1830                    for (n = 0; (p = lsts[n]) != NULL; n++) {
1831
1832                        while (xs_list_iter(&p, &v)) {
1833                            /* already seen? skip */
1834                            if (xs_set_add(&seen, v) == 0)
1835                                continue;
1836
1837                            xs *actor = NULL;
1838
1839                            if (valid_status(object_get(v, &actor))) {
1840                                const char *uname = xs_dict_get(actor, "preferredUsername");
1841
1842                                if (!xs_is_null(uname)) {
1843                                    xs *luname = xs_tolower_i(xs_dup(uname));
1844
1845                                    if (xs_startswith(luname, q)) {
1846                                        xs *acct = mastoapi_account(&snac1, actor);
1847
1848                                        out = xs_list_append(out, acct);
1849                                    }
1850                                }
1851                            }
1852                        }
1853                    }
1854
1855                    /* local users */
1856                    p = ulst;
1857                    while (xs_list_iter(&p, &v)) {
1858                        snac user;
1859
1860                        /* skip this same user */
1861                        if (strcmp(v, xs_dict_get(snac1.config, "uid")) == 0)
1862                            continue;
1863
1864                        /* skip if the uid does not start with the query */
1865                        xs *v2 = xs_tolower_i(xs_dup(v));
1866                        if (!xs_startswith(v2, q))
1867                            continue;
1868
1869                        if (user_open(&user, v)) {
1870                            /* if it's not already seen, add it */
1871                            if (xs_set_add(&seen, user.actor) == 1) {
1872                                xs *actor = msg_actor(&user);
1873                                xs *acct  = mastoapi_account(&snac1, actor);
1874
1875                                out = xs_list_append(out, acct);
1876                            }
1877
1878                            user_free(&user);
1879                        }
1880                    }
1881
1882                    xs_set_free(&seen);
1883                }
1884            }
1885            else
1886            /* is it a local user? */
1887            if (user_open(&snac2, uid) || user_open_by_md5(&snac2, uid)) {
1888                if (opt == NULL) {
1889                    /* account information */
1890                    actor = msg_actor(&snac2);
1891                    out   = mastoapi_account(NULL, actor);
1892                }
1893                else
1894                if (strcmp(opt, "statuses") == 0) { /** **/
1895                    /* the public list of posts of a user */
1896                    const char *limit_s  = xs_dict_get(args, "limit");
1897                    const char *o_max_id = xs_dict_get(args, "max_id");
1898                    int limit = limit_s ? atoi(limit_s) : 20;
1899                    xs *max_id = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL;
1900
1901                    srv_debug(1, xs_fmt("account statuses: max_id=%s limit=%d", max_id ? max_id : "(null)", limit));
1902
1903                    xs *timeline = timeline_simple_list(&snac2, "public", 0, 256, NULL);
1904                    xs_list *p   = timeline;
1905                    const xs_str *v;
1906                    xs_set seen;
1907                    int cnt = 0;
1908                    int skip_until_max = max_id != NULL;
1909
1910                    out = xs_list_new();
1911                    xs_set_init(&seen);
1912
1913                    while (xs_list_iter(&p, &v) && cnt < limit) {
1914                        xs *msg = NULL;
1915
1916                        if (valid_status(timeline_get_by_md5(&snac2, v, &msg))) {
1917                            const char *msg_id = xs_dict_get(msg, "id");
1918
1919                            /* add only posts by the author */
1920                            if (!xs_is_null(msg_id) &&
1921                                strcmp(xs_dict_get(msg, "type"), "Note") == 0 &&
1922                                is_msg_mine(&snac2, xs_dict_get(msg, "id")) && is_msg_public(msg)) {
1923
1924                                /* if max_id is set, skip entries until we find it */
1925                                if (skip_until_max) {
1926                                    xs *mid = mastoapi_id(msg);
1927                                    if (strcmp(mid, max_id) == 0) {
1928                                        skip_until_max = 0;
1929                                        srv_debug(2, xs_fmt("account statuses: found max_id, starting from next post"));
1930                                    }
1931                                    continue;
1932                                }
1933
1934                                /* deduplicate by message id */
1935                                if (xs_set_add(&seen, msg_id) == 1) {
1936                                    xs *st = mastoapi_status(&snac2, msg);
1937
1938                                    if (st) {
1939                                        out = xs_list_append(out, st);
1940                                        cnt++;
1941                                    }
1942                                }
1943                            }
1944                        }
1945                    }
1946
1947                    srv_debug(1, xs_fmt("account statuses: returning %d posts (requested %d)", cnt, limit));
1948                    xs_set_free(&seen);
1949                }
1950                else
1951                if (strcmp(opt, "featured_tags") == 0) {
1952                    /* snac doesn't have features tags, yet? */
1953                    /* implement empty response so apps like Tokodon don't show an error */
1954                    out = xs_list_new();
1955                }
1956                else
1957                if (strcmp(opt, "following") == 0) {
1958                    xs *wing = following_list(&snac1);
1959                    out = xs_list_new();
1960                    int c = 0;
1961                    const char *v;
1962
1963                    while (xs_list_next(wing, &v, &c)) {
1964                        xs *actor = NULL;
1965
1966                        if (valid_status(object_get(v, &actor))) {
1967                            xs *acct = mastoapi_account(NULL, actor);
1968                            out = xs_list_append(out, acct);
1969                        }
1970                    }
1971                }
1972                else
1973                if (strcmp(opt, "followers") == 0) {
1974                    out = xs_list_new();
1975                }
1976                else
1977                if (strcmp(opt, "lists") == 0) {
1978                    out = mastoapi_account_lists(&snac1, uid);
1979                }
1980                else
1981                if (strcmp(opt, "familiar_followers") == 0) {
1982                    /* familiar followers - not implemented, return empty array */
1983                    out = xs_list_new();
1984                }
1985
1986                user_free(&snac2);
1987            }
1988            else {
1989                /* try the uid as the md5 of a possibly loaded actor */
1990                if (logged_in && valid_status(object_get_by_md5(uid, &actor))) {
1991                    if (opt == NULL) {
1992                        /* account information */
1993                        out = mastoapi_account(&snac1, actor);
1994                    }
1995                    else
1996                    if (strcmp(opt, "statuses") == 0) {
1997                        /* fetch statuses from remote outbox */
1998                        out = xs_list_new();
1999                        const char *outbox_url = xs_dict_get(actor, "outbox");
2000
2001                        if (!xs_is_null(outbox_url)) {
2002                            /* extract query parameters */
2003                            const char *limit_s = xs_dict_get(args, "limit");
2004                            const char *exclude_replies_s = xs_dict_get(args, "exclude_replies");
2005                            const char *o_max_id = xs_dict_get(args, "max_id");
2006
2007                            int limit = 20;
2008                            if (!xs_is_null(limit_s))
2009                                limit = atoi(limit_s);
2010                            if (limit == 0 || limit > 40)
2011                                limit = 20;
2012
2013                            int exclude_replies = !xs_is_null(exclude_replies_s) &&
2014                                                 strcmp(exclude_replies_s, "true") == 0;
2015
2016                            xs *max_id = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL;
2017                            int skip_until_max = max_id != NULL;
2018
2019                            srv_debug(1, xs_fmt("remote account statuses: fetching from %s (max_id=%s limit=%d)",
2020                                               outbox_url, max_id ? max_id : "(null)", limit));
2021
2022                            /* fetch first page only - safer for memory on large instances */
2023                            xs *outbox_collection = NULL;
2024                            xs *first_page = NULL;
2025                            const xs_list *items = get_collection_items(&snac1, outbox_url,
2026                                                                        &outbox_collection, &first_page);
2027
2028                            int count = 0;
2029                            int processed = 0;
2030
2031                            if (!xs_is_null(items) && xs_type(items) == XSTYPE_LIST) {
2032                                int total_items = xs_list_len(items);
2033                                srv_debug(1, xs_fmt("remote account statuses: got %d items from outbox", total_items));
2034
2035                                const xs_val *item;
2036
2037                                xs_list_foreach(items, item) {
2038                                    processed++;
2039
2040                                    if (count >= limit)
2041                                        break;
2042
2043                                    const xs_dict *post = extract_post_from_item(item);
2044                                    if (!post)
2045                                        continue;
2046
2047                                    const char *post_type = xs_dict_get(post, "type");
2048                                    const char *in_reply_to = xs_dict_get(post, "inReplyTo");
2049
2050                                    /* apply filters */
2051                                    if (exclude_replies && !xs_is_null(in_reply_to))
2052                                        continue;
2053
2054                                    if (is_valid_post_type(post_type)) {
2055                                        /* store object locally so mastoapi_id() can generate valid IDs */
2056                                        const char *post_id = xs_dict_get(post, "id");
2057                                        if (!xs_is_null(post_id))
2058                                            object_add(post_id, post);
2059
2060                                        /* handle pagination with max_id */
2061                                        if (skip_until_max) {
2062                                            xs *mid = mastoapi_id(post);
2063                                            if (!xs_is_null(mid) && strcmp(mid, max_id) == 0) {
2064                                                skip_until_max = 0;
2065                                                srv_debug(2, xs_fmt("remote account statuses: found max_id at position %d", processed));
2066                                            }
2067                                            continue;
2068                                        }
2069
2070                                        /* pass logged-in user context to enable media proxying if configured */
2071                                        xs *st = mastoapi_status(&snac1, post);
2072                                        if (st) {
2073                                            out = xs_list_append(out, st);
2074                                            count++;
2075                                        }
2076                                    }
2077                                }
2078                            }
2079                            else {
2080                                srv_debug(1, xs_fmt("remote account statuses: no items found in outbox"));
2081                            }
2082
2083                            srv_debug(1, xs_fmt("remote account statuses: processed %d items, returning %d posts (requested %d)",
2084                                               processed, count, limit));
2085                        }
2086                    }
2087                    else
2088                    if (strcmp(opt, "featured_tags") == 0) {
2089                        /* snac doesn't have features tags, yet? */
2090                        /* implement empty response so apps like Tokodon don't show an error */
2091                        out = xs_list_new();
2092                    }
2093                    else
2094                    if (strcmp(opt, "lists") == 0) {
2095                        out = mastoapi_account_lists(&snac1, uid);
2096                    }
2097                    else
2098                    if (strcmp(opt, "familiar_followers") == 0) {
2099                        /* familiar followers - not implemented, return empty array */
2100                        out = xs_list_new();
2101                    }
2102                }
2103            }
2104
2105            if (out != NULL) {
2106                *body  = xs_json_dumps(out, 4);
2107                *ctype = "application/json";
2108                status = HTTP_STATUS_OK;
2109            }
2110        }
2111    }
2112    else
2113    if (strcmp(cmd, "/v1/timelines/home") == 0) { /** **/
2114        /* the private timeline */
2115        if (logged_in) {
2116            xs *ifn = user_index_fn(&snac1, "private");
2117            xs *out = mastoapi_timeline(&snac1, args, ifn);
2118
2119            *link = timeline_link_header("/api/v1/timelines/home", out);
2120
2121            *body  = xs_json_dumps(out, 4);
2122            *ctype = "application/json";
2123            status = HTTP_STATUS_OK;
2124
2125            srv_debug(2, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out)));
2126        }
2127        else {
2128            status = HTTP_STATUS_UNAUTHORIZED;
2129        }
2130    }
2131    else
2132    if (strcmp(cmd, "/v1/timelines/public") == 0) { /** **/
2133        /* the instance public timeline (public timelines for all users) */
2134        xs *ifn = instance_index_fn();
2135        xs *out = mastoapi_timeline(NULL, args, ifn);
2136
2137        *body  = xs_json_dumps(out, 4);
2138        *ctype = "application/json";
2139        status = HTTP_STATUS_OK;
2140    }
2141    else
2142    if (xs_startswith(cmd, "/v1/timelines/tag/")) { /** **/
2143        /* get the tag */
2144        xs *l = xs_split(cmd, "/");
2145        const char *tag = xs_list_get(l, -1);
2146
2147        xs *ifn = tag_fn(tag);
2148        xs *out = mastoapi_timeline(NULL, args, ifn);
2149
2150        *body  = xs_json_dumps(out, 4);
2151        *ctype = "application/json";
2152        status = HTTP_STATUS_OK;
2153    }
2154    else
2155    if (xs_startswith(cmd, "/v1/timelines/list/")) { /** **/
2156        /* get the list id */
2157        if (logged_in) {
2158            xs *l = xs_split(cmd, "/");
2159            const char *list = xs_list_get(l, -1);
2160
2161            xs *ifn = list_timeline_fn(&snac1, list);
2162            xs *out = mastoapi_timeline(NULL, args, ifn);
2163
2164            *body  = xs_json_dumps(out, 4);
2165            *ctype = "application/json";
2166            status = HTTP_STATUS_OK;
2167        }
2168        else
2169            status = HTTP_STATUS_MISDIRECTED_REQUEST;
2170    }
2171    else
2172    if (strcmp(cmd, "/v1/conversations") == 0) { /** **/
2173        /* TBD */
2174        *body  = xs_dup("[]");
2175        *ctype = "application/json";
2176        status = HTTP_STATUS_OK;
2177    }
2178    else
2179    if (strcmp(cmd, "/v1/notifications") == 0) { /** **/
2180        if (logged_in) {
2181            xs *l      = notify_list(&snac1, 0, 64);
2182            xs *out    = xs_list_new();
2183            const char *v;
2184            const xs_list *excl = xs_dict_get(args, "exclude_types[]");
2185            const xs_list *incl = xs_dict_get(args, "types[]");
2186            const char *min_id = xs_dict_get(args, "min_id");
2187            const char *max_id = xs_dict_get(args, "max_id");
2188            const char *limit = xs_dict_get(args, "limit");
2189            int limit_count = 0;
2190            if (xs_is_string(limit)) {
2191                limit_count = atoi(limit);
2192            }
2193
2194            if (dbglevel) {
2195                xs *js = xs_json_dumps(args, 0);
2196                srv_debug(1, xs_fmt("mastoapi_notifications args %s", js));
2197            }
2198
2199            xs_list_foreach(l, v) {
2200                xs *noti = notify_get(&snac1, v);
2201
2202                if (noti == NULL)
2203                    continue;
2204
2205                const char *type  = xs_dict_get(noti, "type");
2206                const char *utype = xs_dict_get(noti, "utype");
2207                const char *objid = xs_dict_get(noti, "objid");
2208                const char *id    = xs_dict_get(noti, "id");
2209                const char *actid = xs_dict_get(noti, "actor");
2210                xs *fid = xs_replace(id, ".", "");
2211                xs *actor = NULL;
2212                xs *entry = NULL;
2213
2214                if (!valid_status(actor_get(actid, &actor)))
2215                    continue;
2216
2217                if (objid != NULL && !valid_status(object_get(objid, &entry)))
2218                    continue;
2219
2220                if (is_hidden(&snac1, objid))
2221                    continue;
2222
2223                if (max_id) {
2224                    if (strcmp(fid, max_id) == 0)
2225                        max_id = NULL;
2226
2227                    continue;
2228                }
2229
2230                if (min_id) {
2231                    if (strcmp(fid, min_id) <= 0) {
2232                        continue;
2233                    }
2234                }
2235
2236                /* convert the type */
2237                if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0)
2238                    type = "favourite";
2239                else
2240                if (strcmp(type, "Announce") == 0)
2241                    type = "reblog";
2242                else
2243                if (strcmp(type, "Follow") == 0)
2244                    type = "follow";
2245                else
2246                if (strcmp(type, "Create") == 0)
2247                    type = "mention";
2248                else
2249                if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
2250                    type = "poll";
2251                else
2252                    continue;
2253
2254                /* excluded type? */
2255                if (xs_is_list(excl) && xs_list_in(excl, type) != -1)
2256                    continue;
2257
2258                /* included type? */
2259                if (xs_is_list(incl) && xs_list_in(incl, type) == -1)
2260                    continue;
2261
2262                xs *mn = xs_dict_new();
2263
2264                mn = xs_dict_append(mn, "type", type);
2265
2266                mn = xs_dict_append(mn, "id", fid);
2267
2268                mn = xs_dict_append(mn, "created_at", xs_dict_get(noti, "date"));
2269
2270                xs *acct = mastoapi_account(&snac1, actor);
2271
2272                if (acct == NULL)
2273                    continue;
2274
2275                mn = xs_dict_append(mn, "account", acct);
2276
2277                if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) {
2278                    xs *st = mastoapi_status(&snac1, entry);
2279
2280                    if (st)
2281                        mn = xs_dict_append(mn, "status", st);
2282                }
2283
2284                out = xs_list_append(out, mn);
2285
2286                if (--limit_count <= 0)
2287                    break;
2288            }
2289
2290            srv_debug(1, xs_fmt("mastoapi_notifications count %d", xs_list_len(out)));
2291
2292            *body  = xs_json_dumps(out, 4);
2293            *ctype = "application/json";
2294            status = HTTP_STATUS_OK;
2295        }
2296        else
2297            status = HTTP_STATUS_UNAUTHORIZED;
2298    }
2299    else
2300    if (strcmp(cmd, "/v1/filters") == 0) { /** **/
2301        /* snac will never have filters */
2302        *body  = xs_dup("[]");
2303        *ctype = "application/json";
2304        status = HTTP_STATUS_OK;
2305    }
2306    else
2307    if (strcmp(cmd, "/v2/filters") == 0) { /** **/
2308        /* snac will never have filters
2309         * but still, without a v2 endpoint a short delay is introduced
2310         * in some apps */
2311        *body  = xs_dup("[]");
2312        *ctype = "application/json";
2313        status = HTTP_STATUS_OK;
2314    }
2315    else
2316    if (strcmp(cmd, "/v1/favourites") == 0) { /** **/
2317        /* snac will never support a list of favourites */
2318        *body  = xs_dup("[]");
2319        *ctype = "application/json";
2320        status = HTTP_STATUS_OK;
2321    }
2322    else
2323    if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/
2324        if (logged_in) {
2325            xs *ifn = bookmark_index_fn(&snac1);
2326            xs *out = mastoapi_timeline(&snac1, args, ifn);
2327
2328            *body  = xs_json_dumps(out, 4);
2329            *ctype = "application/json";
2330            status = HTTP_STATUS_OK;
2331        }
2332        else
2333            status = HTTP_STATUS_UNAUTHORIZED;
2334    }
2335    else
2336    if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
2337        if (logged_in) {
2338            xs *l  = mastoapi_account_lists(&snac1, NULL);
2339
2340            *body  = xs_json_dumps(l, 4);
2341            *ctype = "application/json";
2342            status = HTTP_STATUS_OK;
2343        }
2344        else
2345            status = HTTP_STATUS_UNAUTHORIZED;
2346    }
2347    else
2348    if (xs_startswith(cmd, "/v1/lists/")) { /** list information **/
2349        if (logged_in) {
2350            xs *l = xs_split(cmd, "/");
2351            const char *p = xs_list_get(l, -1);
2352
2353            if (p) {
2354                if (strcmp(p, "accounts") == 0) {
2355                    p = xs_list_get(l, -2);
2356
2357                    if (p && xs_is_hex(p)) {
2358                        xs *actors = list_members(&snac1, p, NULL, 0);
2359                        xs *out = xs_list_new();
2360                        int c = 0;
2361                        const char *v;
2362
2363                        while (xs_list_next(actors, &v, &c)) {
2364                            xs *actor = NULL;
2365
2366                            if (valid_status(object_get_by_md5(v, &actor))) {
2367                                xs *acct = mastoapi_account(&snac1, actor);
2368                                out = xs_list_append(out, acct);
2369                            }
2370                        }
2371
2372                        *body  = xs_json_dumps(out, 4);
2373                        *ctype = "application/json";
2374                        status = HTTP_STATUS_OK;
2375                    }
2376                }
2377                else
2378                if (xs_is_hex(p)) {
2379                    xs *out = xs_list_new();
2380                    xs *lol = list_maint(&snac1, NULL, 0);
2381                    int c = 0;
2382                    const xs_list *v;
2383
2384                    while (xs_list_next(lol, &v, &c)) {
2385                        const char *id = xs_list_get(v, 0);
2386
2387                        if (id && strcmp(id, p) == 0) {
2388                            xs *d = xs_dict_new();
2389
2390                            d = xs_dict_append(d, "id", p);
2391                            d = xs_dict_append(d, "title", xs_list_get(v, 1));
2392                            d = xs_dict_append(d, "replies_policy", "list");
2393                            d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
2394
2395                            out = xs_dup(d);
2396                            break;
2397                        }
2398                    }
2399
2400                    *body  = xs_json_dumps(out, 4);
2401                    *ctype = "application/json";
2402                    status = HTTP_STATUS_OK;
2403                }
2404            }
2405        }
2406        else
2407            status = HTTP_STATUS_UNAUTHORIZED;
2408    }
2409    else
2410    if (strcmp(cmd, "/v1/scheduled_statuses") == 0) { /** **/
2411        /* TBD */
2412        *body  = xs_dup("[]");
2413        *ctype = "application/json";
2414        status = HTTP_STATUS_OK;
2415    }
2416    else
2417    if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/
2418        if (logged_in) {
2419            xs *pend = pending_list(&snac1);
2420            xs *resp = xs_list_new();
2421            const char *id;
2422
2423            xs_list_foreach(pend, id) {
2424                xs *actor = NULL;
2425
2426                if (valid_status(object_get(id, &actor))) {
2427                    xs *acct = mastoapi_account(&snac1, actor);
2428
2429                    if (acct)
2430                        resp = xs_list_append(resp, acct);
2431                }
2432            }
2433
2434            *body  = xs_json_dumps(resp, 4);
2435            *ctype = "application/json";
2436            status = HTTP_STATUS_OK;
2437        }
2438    }
2439    else
2440    if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
2441        if (logged_in) {
2442            xs *resp = xs_list_new();
2443            double la = 0.0;
2444            xs *user_la = xs_dup(xs_dict_get(snac1.config, "last_announcement"));
2445            if (user_la != NULL)
2446                la = xs_number_get(user_la);
2447            xs *val_date = xs_str_utctime(la, ISO_DATE_SPEC);
2448
2449            /* contrary to html, we always send the announcement and set the read flag instead */
2450
2451            const t_announcement *annce = announcement(la);
2452            if (annce != NULL && annce->text != NULL) {
2453                xs *an = xs_dict_new();
2454                xs *id = xs_fmt("%d", annce->timestamp);
2455                xs *ct = xs_fmt("<p>%s</p>", annce->text);
2456
2457                an = xs_dict_set(an, "id",           id);
2458                an = xs_dict_set(an, "content",      ct);
2459                an = xs_dict_set(an, "starts_at",    xs_stock(XSTYPE_NULL));
2460                an = xs_dict_set(an, "ends_at",      xs_stock(XSTYPE_NULL));
2461                an = xs_dict_set(an, "all_day",      xs_stock(XSTYPE_TRUE));
2462                an = xs_dict_set(an, "published_at", val_date);
2463                an = xs_dict_set(an, "updated_at",   val_date);
2464                an = xs_dict_set(an, "read",         (annce->timestamp >= la)
2465                    ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE));
2466                an = xs_dict_set(an, "mentions",     xs_stock(XSTYPE_LIST));
2467                an = xs_dict_set(an, "statuses",     xs_stock(XSTYPE_LIST));
2468                an = xs_dict_set(an, "tags",         xs_stock(XSTYPE_LIST));
2469                an = xs_dict_set(an, "emojis",       xs_stock(XSTYPE_LIST));
2470                an = xs_dict_set(an, "reactions",    xs_stock(XSTYPE_LIST));
2471                resp = xs_list_append(resp, an);
2472            }
2473
2474            *body  = xs_json_dumps(resp, 4);
2475            *ctype = "application/json";
2476            status = HTTP_STATUS_OK;
2477        }
2478    }
2479    else
2480    if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
2481        xs *emo = emojis();
2482        xs *list = xs_list_new();
2483        int c = 0;
2484        const xs_str *k;
2485        const xs_val *v;
2486        while(xs_dict_next(emo, &k, &v, &c)) {
2487            xs *current = xs_dict_new();
2488            if (xs_startswith(v, "https://") && xs_startswith((xs_mime_by_ext(v)), "image/")) {
2489                /* remove first and last colon */
2490                xs *shortcode = xs_replace(k, ":", "");
2491                current = xs_dict_append(current, "shortcode", shortcode);
2492                current = xs_dict_append(current, "url", v);
2493                current = xs_dict_append(current, "static_url", v);
2494                current = xs_dict_append(current, "visible_in_picker", xs_stock(XSTYPE_TRUE));
2495                list = xs_list_append(list, current);
2496            }
2497        }
2498        *body  = xs_json_dumps(list, 0);
2499        *ctype = "application/json";
2500        status = HTTP_STATUS_OK;
2501    }
2502    else
2503    if (strcmp(cmd, "/v1/instance") == 0) { /** **/
2504        /* returns an instance object */
2505        xs *ins = xs_dict_new();
2506        const char *host  = xs_dict_get(srv_config, "host");
2507        const char *title = xs_dict_get(srv_config, "title");
2508        const char *sdesc = xs_dict_get(srv_config, "short_description");
2509
2510        ins = xs_dict_append(ins, "uri",         host);
2511        ins = xs_dict_append(ins, "domain",      host);
2512        ins = xs_dict_append(ins, "title",       title && *title ? title : host);
2513        ins = xs_dict_append(ins, "version",     "4.0.0 (not true; really " USER_AGENT ")");
2514        ins = xs_dict_append(ins, "source_url",  WHAT_IS_SNAC_URL);
2515        ins = xs_dict_append(ins, "description", host);
2516
2517        ins = xs_dict_append(ins, "short_description", sdesc && *sdesc ? sdesc : host);
2518
2519        xs *susie = xs_fmt("%s/susie.png", srv_baseurl);
2520        ins = xs_dict_append(ins, "thumbnail", susie);
2521
2522        const char *v = xs_dict_get(srv_config, "admin_email");
2523        if (xs_is_null(v) || *v == '\0')
2524            v = "admin@localhost";
2525
2526        ins = xs_dict_append(ins, "email", v);
2527
2528        ins = xs_dict_append(ins, "rules", xs_stock(XSTYPE_LIST));
2529
2530        xs *l1 = xs_list_append(xs_list_new(), "en");
2531        ins = xs_dict_append(ins, "languages", l1);
2532
2533        xs *wss = xs_fmt("wss:/" "/%s", xs_dict_get(srv_config, "host"));
2534        xs *urls = xs_dict_new();
2535        urls = xs_dict_append(urls, "streaming_api", wss);
2536
2537        ins = xs_dict_append(ins, "urls", urls);
2538
2539        xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock(0));
2540        d2 = xs_dict_append(d2, "status_count", xs_stock(0));
2541        d2 = xs_dict_append(d2, "domain_count", xs_stock(0));
2542        ins = xs_dict_append(ins, "stats", d2);
2543
2544        ins = xs_dict_append(ins, "registrations",     xs_stock(XSTYPE_FALSE));
2545        ins = xs_dict_append(ins, "approval_required", xs_stock(XSTYPE_FALSE));
2546        ins = xs_dict_append(ins, "invites_enabled",   xs_stock(XSTYPE_FALSE));
2547
2548        xs *cfg = xs_dict_new();
2549
2550        {
2551            xs *d11 = xs_json_loads("{\"characters_reserved_per_url\":32,"
2552                "\"max_characters\":100000,\"max_media_attachments\":4}");
2553
2554            const xs_number *max_attachments = xs_dict_get(srv_config, "max_attachments");
2555            if (xs_type(max_attachments) == XSTYPE_NUMBER)
2556                d11 = xs_dict_set(d11, "max_media_attachments", max_attachments);
2557
2558            cfg = xs_dict_append(cfg, "statuses", d11);
2559
2560            xs *d12 = xs_json_loads("{\"max_featured_tags\":0}");
2561            cfg = xs_dict_append(cfg, "accounts", d12);
2562
2563            xs *d13 = xs_json_loads("{\"image_matrix_limit\":33177600,"
2564                        "\"image_size_limit\":16777216,"
2565                        "\"video_frame_rate_limit\":120,"
2566                        "\"video_matrix_limit\":8294400,"
2567                        "\"video_size_limit\":103809024}"
2568            );
2569
2570            {
2571                /* get the supported mime types from the internal list */
2572                const char **p = xs_mime_types;
2573                xs_set mtypes;
2574                xs_set_init(&mtypes);
2575
2576                while (*p) {
2577                    const char *type = p[1];
2578
2579                    if (xs_startswith(type, "image/") ||
2580                        xs_startswith(type, "video/") ||
2581                        xs_startswith(type, "audio/"))
2582                        xs_set_add(&mtypes, type);
2583
2584                    p += 2;
2585                }
2586
2587                xs *l = xs_set_result(&mtypes);
2588                d13 = xs_dict_append(d13, "supported_mime_types", l);
2589            }
2590
2591            cfg = xs_dict_append(cfg, "media_attachments", d13);
2592
2593            xs *d14 = xs_json_loads("{\"max_characters_per_option\":50,"
2594                "\"max_expiration\":2629746,"
2595                "\"max_options\":8,\"min_expiration\":300}");
2596            cfg = xs_dict_append(cfg, "polls", d14);
2597        }
2598
2599        ins = xs_dict_append(ins, "configuration", cfg);
2600
2601        const char *admin_account = xs_dict_get(srv_config, "admin_account");
2602
2603        if (!xs_is_null(admin_account) && *admin_account) {
2604            snac admin;
2605
2606            if (user_open(&admin, admin_account)) {
2607                xs *actor = msg_actor(&admin);
2608                xs *acct  = mastoapi_account(NULL, actor);
2609
2610                ins = xs_dict_append(ins, "contact_account", acct);
2611
2612                user_free(&admin);
2613            }
2614        }
2615
2616        *body  = xs_json_dumps(ins, 4);
2617        *ctype = "application/json";
2618        status = HTTP_STATUS_OK;
2619    }
2620    else
2621    if (strcmp(cmd, "/v1/instance/peers") == 0) { /** **/
2622        /* get the collected inbox list as the instances "this domain is aware of" */
2623        xs *list = inbox_list();
2624        xs *peers = xs_list_new();
2625        const char *inbox;
2626
2627        xs_list_foreach(list, inbox) {
2628            xs *l = xs_split(inbox, "/");
2629            const char *domain = xs_list_get(l, 2);
2630
2631            if (xs_is_string(domain))
2632                peers = xs_list_append(peers, domain);
2633        }
2634
2635        *body  = xs_json_dumps(peers, 4);
2636        *ctype = "application/json";
2637        status = HTTP_STATUS_OK;
2638    }
2639    else
2640    if (strcmp(cmd, "/v1/instance/extended_description") == 0) { /** **/
2641        xs *d = xs_dict_new();
2642        xs *greeting = xs_fmt("%s/greeting.html", srv_basedir);
2643        time_t t = mtime(greeting);
2644        xs *updated_at = xs_str_iso_date(t);
2645        xs *content = xs_replace(snac_blurb, "%host%", xs_dict_get(srv_config, "host"));
2646
2647        d = xs_dict_set(d, "updated_at", updated_at);
2648        d = xs_dict_set(d, "content", content);
2649
2650        *body  = xs_json_dumps(d, 4);
2651        *ctype = "application/json";
2652        status = HTTP_STATUS_OK;
2653    }
2654    else
2655    if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
2656        /* information about a status */
2657        if (logged_in) {
2658            xs *l = xs_split(cmd, "/");
2659            const char *oid = xs_list_get(l, 3);
2660            const char *op = xs_list_get(l, 4);
2661
2662            if (!xs_is_null(oid)) {
2663                xs *msg = NULL;
2664                xs *out = NULL;
2665
2666                /* skip the 'fake' part of the id */
2667                oid = MID_TO_MD5(oid);
2668
2669                xs *id = xs_tolower_i(xs_dup(oid));
2670
2671                if (valid_status(object_get_by_md5(id, &msg))) {
2672                    if (op == NULL) {
2673                        if (!is_muted(&snac1, get_atto(msg))) {
2674                            /* return the status itself */
2675                            out = mastoapi_status(&snac1, msg);
2676                        }
2677                    }
2678                    else
2679                    if (strcmp(op, "context") == 0) { /** **/
2680                        /* return ancestors and children */
2681                        xs *anc = xs_list_new();
2682                        xs *des = xs_list_new();
2683                        xs_list *p;
2684                        const xs_str *v;
2685                        char pid[MD5_HEX_SIZE];
2686
2687                        /* build the [grand]parent list, moving up */
2688                        strncpy(pid, id, sizeof(pid));
2689
2690                        while (object_parent(pid, pid)) {
2691                            xs *m2 = NULL;
2692
2693                            if (valid_status(timeline_get_by_md5(&snac1, pid, &m2))) {
2694                                xs *st = mastoapi_status(&snac1, m2);
2695
2696                                if (st)
2697                                    anc = xs_list_insert(anc, 0, st);
2698                            }
2699                            else
2700                                break;
2701                        }
2702
2703                        /* build the children list */
2704                        xs *children = object_children(xs_dict_get(msg, "id"));
2705                        p = children;
2706
2707                        while (xs_list_iter(&p, &v)) {
2708                            xs *m2 = NULL;
2709
2710                            if (valid_status(timeline_get_by_md5(&snac1, v, &m2))) {
2711                                if (xs_is_null(xs_dict_get(m2, "name"))) {
2712                                    xs *st = mastoapi_status(&snac1, m2);
2713
2714                                    if (st)
2715                                        des = xs_list_append(des, st);
2716                                }
2717                            }
2718                        }
2719
2720                        out = xs_dict_new();
2721                        out = xs_dict_append(out, "ancestors",   anc);
2722                        out = xs_dict_append(out, "descendants", des);
2723                    }
2724                    else
2725                    if (strcmp(op, "reblogged_by") == 0 || /** **/
2726                        strcmp(op, "favourited_by") == 0) { /** **/
2727                        /* return the list of people who liked or boosted this */
2728                        out = xs_list_new();
2729
2730                        xs *l = NULL;
2731
2732                        if (op[0] == 'r')
2733                            l = object_announces(xs_dict_get(msg, "id"));
2734                        else
2735                            l = object_likes(xs_dict_get(msg, "id"));
2736
2737                        xs_list *p = l;
2738                        const xs_str *v;
2739
2740                        while (xs_list_iter(&p, &v)) {
2741                            xs *actor2 = NULL;
2742
2743                            if (valid_status(object_get_by_md5(v, &actor2))) {
2744                                xs *acct2 = mastoapi_account(&snac1, actor2);
2745
2746                                out = xs_list_append(out, acct2);
2747                            }
2748                        }
2749                    }
2750                    else
2751                    if (strcmp(op, "source") == 0) { /** **/
2752                        out = xs_dict_new();
2753
2754                        /* get the mastoapi status id */
2755                        out = xs_dict_append(out, "id", xs_list_get(l, 3));
2756
2757                        out = xs_dict_append(out, "text", xs_dict_get(msg, "sourceContent"));
2758                        out = xs_dict_append(out, "spoiler_text", xs_dict_get(msg, "summary"));
2759                    }
2760                }
2761                else
2762                    srv_debug(1, xs_fmt("mastoapi status: bad id %s", id));
2763
2764                if (out != NULL) {
2765                    *body  = xs_json_dumps(out, 4);
2766                    *ctype = "application/json";
2767                    status = HTTP_STATUS_OK;
2768                }
2769            }
2770        }
2771        else
2772            status = HTTP_STATUS_UNAUTHORIZED;
2773    }
2774    else
2775    if (strcmp(cmd, "/v1/preferences") == 0) { /** **/
2776        /* TBD */
2777        *body  = xs_dup("{}");
2778        *ctype = "application/json";
2779        status = HTTP_STATUS_OK;
2780    }
2781    else
2782    if (strcmp(cmd, "/v1/markers") == 0) { /** **/
2783        if (logged_in) {
2784            const xs_list *timeline = xs_dict_get(args, "timeline[]");
2785            xs_str *json = NULL;
2786            if (!xs_is_null(timeline))
2787                json = xs_json_dumps(markers_get(&snac1, timeline), 4);
2788
2789            if (!xs_is_null(json))
2790                *body = json;
2791            else
2792                *body = xs_dup("{}");
2793
2794            *ctype = "application/json";
2795            status = HTTP_STATUS_OK;
2796        }
2797        else
2798            status = HTTP_STATUS_UNAUTHORIZED;
2799    }
2800    else
2801    if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
2802        if (logged_in) {
2803            xs *r = xs_list_new();
2804            const xs_list *followed_hashtags = xs_dict_get_def(snac1.config,
2805                        "followed_hashtags", xs_stock(XSTYPE_LIST));
2806            const char *hashtag;
2807
2808            xs_list_foreach(followed_hashtags, hashtag) {
2809                if (*hashtag == '#') {
2810                    xs *d = xs_dict_new();
2811                    xs *s = xs_fmt("%s?t=%s", srv_baseurl, hashtag + 1);
2812
2813                    d = xs_dict_set(d, "name", hashtag + 1);
2814                    d = xs_dict_set(d, "url", s);
2815                    d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST));
2816
2817                    r = xs_list_append(r, d);
2818                }
2819            }
2820
2821            *body  = xs_json_dumps(r, 4);
2822            *ctype = "application/json";
2823            status = HTTP_STATUS_OK;
2824        }
2825        else
2826            status = HTTP_STATUS_UNAUTHORIZED;
2827    }
2828    else
2829    if (strcmp(cmd, "/v1/blocks") == 0) { /** **/
2830        *body  = xs_dup("[]");
2831        *ctype = "application/json";
2832        status = HTTP_STATUS_OK;
2833    }
2834    else
2835    if (strcmp(cmd, "/v1/mutes") == 0) { /** **/
2836        *body  = xs_dup("[]");
2837        *ctype = "application/json";
2838        status = HTTP_STATUS_OK;
2839    }
2840    else
2841    if (strcmp(cmd, "/v1/trends/tags") == 0) { /** **/
2842        *body  = xs_dup("[]");
2843        *ctype = "application/json";
2844        status = HTTP_STATUS_OK;
2845    }
2846    else
2847    if (strcmp(cmd, "/v1/trends/statuses") == 0) { /** **/
2848        *body  = xs_dup("[]");
2849        *ctype = "application/json";
2850        status = HTTP_STATUS_OK;
2851    }
2852    else
2853    if (strcmp(cmd, "/v2/search") == 0) { /** **/
2854        if (logged_in) {
2855            const char *q      = xs_dict_get(args, "q");
2856            const char *type   = xs_dict_get(args, "type");
2857            const char *offset = xs_dict_get(args, "offset");
2858
2859            xs *acl = xs_list_new();
2860            xs *stl = xs_list_new();
2861            xs *htl = xs_list_new();
2862            xs *res = xs_dict_new();
2863
2864            if (xs_is_null(offset) || strcmp(offset, "0") == 0) {
2865                /* reply something only for offset 0; otherwise,
2866                   apps like Tusky keep asking again and again */
2867                if (xs_startswith(q, "https://")) {
2868                    if (!timeline_here(&snac1, q)) {
2869                        xs *object = NULL;
2870                        int status;
2871
2872                        status = activitypub_request(&snac1, q, &object);
2873                        snac_debug(&snac1, 1, xs_fmt("Request searched URL %s %d", q, status));
2874
2875                        if (valid_status(status)) {
2876                            /* got it; also request the actor */
2877                            const char *attr_to = get_atto(object);
2878                            xs *actor_obj = NULL;
2879
2880                            if (!xs_is_null(attr_to)) {
2881                                status = actor_request(&snac1, attr_to, &actor_obj);
2882
2883                                snac_debug(&snac1, 1, xs_fmt("Request author %s of %s %d", attr_to, q, status));
2884
2885                                if (valid_status(status)) {
2886                                    /* add the actor */
2887                                    actor_add(attr_to, actor_obj);
2888
2889                                    /* add the post to the timeline */
2890                                    timeline_add(&snac1, q, object);
2891                                }
2892                            }
2893                        }
2894                    }
2895                }
2896
2897                if (!xs_is_null(q)) {
2898                    if (xs_is_null(type) || strcmp(type, "accounts") == 0) {
2899                        /* do a webfinger query */
2900                        char *actor = NULL;
2901                        char *user  = NULL;
2902
2903                        if (valid_status(webfinger_request(q, &actor, &user)) && actor) {
2904                            xs *actor_o = NULL;
2905
2906                            if (valid_status(actor_request(&snac1, actor, &actor_o))) {
2907                                xs *acct = mastoapi_account(NULL, actor_o);
2908
2909                                acl = xs_list_append(acl, acct);
2910
2911                                if (!object_here(actor))
2912                                    object_add(actor, actor_o);
2913                            }
2914                        }
2915                    }
2916
2917                    if (xs_is_null(type) || strcmp(type, "hashtags") == 0) {
2918                        /* search this tag */
2919                        xs *tl = tag_search((char *)q, 0, 1);
2920
2921                        if (xs_list_len(tl)) {
2922                            xs *d = xs_dict_new();
2923
2924                            d = xs_dict_append(d, "name", q);
2925                            xs *url = xs_fmt("%s?t=%s", srv_baseurl, q);
2926                            d = xs_dict_append(d, "url", url);
2927                            d = xs_dict_append(d, "history", xs_stock(XSTYPE_LIST));
2928
2929                            htl = xs_list_append(htl, d);
2930                        }
2931                    }
2932
2933                    if (xs_is_null(type) || strcmp(type, "statuses") == 0) {
2934                        int to = 0;
2935                        int cnt = 40;
2936                        xs *tl = content_search(&snac1, q, 1, 0, cnt, 0, &to);
2937                        int c = 0;
2938                        const char *v;
2939
2940                        while (xs_list_next(tl, &v, &c) && --cnt) {
2941                            xs *post = NULL;
2942
2943                            if (!valid_status(timeline_get_by_md5(&snac1, v, &post)))
2944                                continue;
2945
2946                            xs *s = mastoapi_status(&snac1, post);
2947
2948                            if (!xs_is_null(s))
2949                                stl = xs_list_append(stl, s);
2950                        }
2951                    }
2952                }
2953            }
2954
2955            res = xs_dict_append(res, "accounts", acl);
2956            res = xs_dict_append(res, "statuses", stl);
2957            res = xs_dict_append(res, "hashtags", htl);
2958
2959            *body  = xs_json_dumps(res, 4);
2960            *ctype = "application/json";
2961            status = HTTP_STATUS_OK;
2962        }
2963        else
2964            status = HTTP_STATUS_UNAUTHORIZED;
2965    }
2966
2967    /* user cleanup */
2968    if (logged_in)
2969        user_free(&snac1);
2970
2971    srv_debug(1, xs_fmt("mastoapi_get_handler %s %d", q_path, status));
2972
2973    return status;
2974}
2975
2976
2977int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2978                          const char *payload, int p_size,
2979                          char **body, int *b_size, char **ctype)
2980{
2981    (void)p_size;
2982    (void)b_size;
2983
2984    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2985        return 0;
2986
2987    int status    = HTTP_STATUS_NOT_FOUND;
2988    xs *args      = NULL;
2989    const char *i_ctype = xs_dict_get(req, "content-type");
2990
2991    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
2992        if (!xs_is_null(payload)) {
2993            args = xs_json_loads(payload);
2994        }
2995    }
2996    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
2997    {
2998        // Some apps send form data instead of json so we should cater for those
2999        if (!xs_is_null(payload)) {
3000            args    = xs_url_vars(payload);
3001        }
3002    }
3003    else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data"))
3004    {
3005        // Handle multipart/form-data by using p_vars (already parsed by httpd)
3006        args = xs_dup(xs_dict_get(req, "p_vars"));
3007    }
3008
3009    /* if args still NULL, try falling back to p_vars or q_vars */
3010    if (args == NULL)
3011        args = xs_dup(xs_dict_get(req, "p_vars"));
3012
3013    if (args == NULL)
3014        args = xs_dup(xs_dict_get(req, "q_vars"));
3015
3016    if (args == NULL) {
3017        srv_debug(1, xs_fmt("mastoapi_post_handler: failed to parse args for %s, content-type: %s",
3018                           q_path, i_ctype ? i_ctype : "(null)"));
3019        return HTTP_STATUS_BAD_REQUEST;
3020    }
3021
3022    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
3023
3024    snac snac = {0};
3025    int logged_in = process_auth_token(&snac, req);
3026
3027    if (strcmp(cmd, "/v1/apps") == 0) { /** **/
3028        const char *name  = xs_dict_get(args, "client_name");
3029        const char *ruri  = xs_dict_get(args, "redirect_uris");
3030        const char *scope = xs_dict_get(args, "scope");
3031
3032        /* Ice Cubes sends these values as query parameters, so try these */
3033        if (name == NULL && ruri == NULL && scope == NULL) {
3034            args = xs_dup(xs_dict_get(req, "q_vars"));
3035            name  = xs_dict_get(args, "client_name");
3036            ruri  = xs_dict_get(args, "redirect_uris");
3037            scope = xs_dict_get(args, "scope");
3038        }
3039
3040        if (xs_type(ruri) == XSTYPE_LIST)
3041            ruri = xs_dict_get(ruri, 0);
3042
3043        if (name && ruri) {
3044            xs *app  = xs_dict_new();
3045            xs *id   = xs_replace_i(tid(0), ".", "");
3046            xs *csec = random_str();
3047            xs *vkey = random_str();
3048            xs *cid  = NULL;
3049
3050            /* pick a non-existent random cid */
3051            for (;;) {
3052                cid = random_str();
3053                xs *p_app = app_get(cid);
3054
3055                if (p_app == NULL)
3056                    break;
3057
3058                xs_free(cid);
3059            }
3060
3061            app = xs_dict_append(app, "name",          name);
3062            app = xs_dict_append(app, "redirect_uri",  ruri);
3063            app = xs_dict_append(app, "client_id",     cid);
3064            app = xs_dict_append(app, "client_secret", csec);
3065            app = xs_dict_append(app, "vapid_key",     vkey);
3066            app = xs_dict_append(app, "id",            id);
3067
3068            *body  = xs_json_dumps(app, 4);
3069            *ctype = "application/json";
3070            status = HTTP_STATUS_OK;
3071
3072            app = xs_dict_append(app, "code", "");
3073
3074            if (scope)
3075                app = xs_dict_append(app, "scope", scope);
3076
3077            app_add(cid, app);
3078
3079            srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
3080        }
3081    }
3082    else
3083    if (strcmp(cmd, "/v1/statuses") == 0) { /** **/
3084        if (logged_in) {
3085            /* post a new Note */
3086            const char *content    = xs_dict_get(args, "status");
3087            const char *mid        = xs_dict_get(args, "in_reply_to_id");
3088            const char *visibility = xs_dict_get(args, "visibility");
3089            const char *summary    = xs_dict_get(args, "spoiler_text");
3090            const char *media_ids  = xs_dict_get(args, "media_ids");
3091            const char *language   = xs_dict_get(args, "language");
3092
3093            if (xs_is_null(media_ids))
3094                media_ids = xs_dict_get(args, "media_ids[]");
3095
3096            if (xs_is_null(media_ids))
3097                media_ids = xs_dict_get(args, "media_ids");
3098
3099            if (xs_is_null(visibility))
3100                visibility = "public";
3101
3102            xs *attach_list = xs_list_new();
3103            xs *irt         = NULL;
3104
3105            /* is it a reply? */
3106            if (mid != NULL) {
3107                xs *r_msg = NULL;
3108                const char *md5 = MID_TO_MD5(mid);
3109
3110                if (valid_status(object_get_by_md5(md5, &r_msg)))
3111                    irt = xs_dup(xs_dict_get(r_msg, "id"));
3112            }
3113
3114            /* does it have attachments? */
3115            if (!xs_is_null(media_ids)) {
3116                xs *mi = NULL;
3117
3118                if (xs_type(media_ids) == XSTYPE_LIST)
3119                    mi = xs_dup(media_ids);
3120                else {
3121                    mi = xs_list_new();
3122                    mi = xs_list_append(mi, media_ids);
3123                }
3124
3125                xs_list *p = mi;
3126                const xs_str *v;
3127
3128                while (xs_list_iter(&p, &v)) {
3129                    xs *l    = xs_list_new();
3130                    xs *url  = xs_fmt("%s/s/%s", snac.actor, v);
3131                    xs *desc = static_get_meta(&snac, v);
3132
3133                    l = xs_list_append(l, url);
3134                    l = xs_list_append(l, desc);
3135
3136                    attach_list = xs_list_append(attach_list, l);
3137                }
3138            }
3139
3140            /* prepare the message */
3141            int scope = SCOPE_MENTIONED;
3142            if (strcmp(visibility, "unlisted") == 0)
3143                scope = SCOPE_UNLISTED;
3144            else
3145            if (strcmp(visibility, "public") == 0)
3146                scope = SCOPE_PUBLIC;
3147            else
3148            if (strcmp(visibility, "private") == 0)
3149                scope = SCOPE_FOLLOWERS;
3150
3151            xs *msg = msg_note(&snac, content, NULL, irt, attach_list, scope, language, NULL);
3152
3153            if (!xs_is_null(summary) && *summary) {
3154                msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
3155                msg = xs_dict_set(msg, "summary",   summary);
3156            }
3157
3158            /* scheduled? */
3159            const char *scheduled_at = xs_dict_get(args, "scheduled_at");
3160
3161            if (xs_is_string(scheduled_at) && *scheduled_at) {
3162                msg = xs_dict_set(msg, "published", scheduled_at);
3163
3164                schedule_add(&snac, xs_dict_get(msg, "id"), msg);
3165            }
3166            else {
3167                /* store */
3168                timeline_add(&snac, xs_dict_get(msg, "id"), msg);
3169
3170                /* 'Create' message */
3171                xs *c_msg = msg_create(&snac, msg);
3172                enqueue_message(&snac, c_msg);
3173
3174                timeline_touch(&snac);
3175            }
3176
3177            /* convert to a mastodon status as a response code */
3178            xs *st = mastoapi_status(&snac, msg);
3179
3180            *body  = xs_json_dumps(st, 4);
3181            *ctype = "application/json";
3182            status = HTTP_STATUS_OK;
3183        }
3184        else
3185            status = HTTP_STATUS_UNAUTHORIZED;
3186    }
3187    else
3188    if (xs_startswith(cmd, "/v1/statuses")) { /** **/
3189        if (logged_in) {
3190            /* operations on a status */
3191            xs *l = xs_split(cmd, "/");
3192            const char *mid = xs_list_get(l, 3);
3193            const char *op  = xs_list_get(l, 4);
3194
3195            if (!xs_is_null(mid)) {
3196                xs *msg = NULL;
3197                xs *out = NULL;
3198
3199                /* skip the 'fake' part of the id */
3200                mid = MID_TO_MD5(mid);
3201
3202                /* try timeline first, then global object store for remote posts */
3203                int found = valid_status(timeline_get_by_md5(&snac, mid, &msg));
3204                if (!found)
3205                    found = valid_status(object_get_by_md5(mid, &msg));
3206
3207                if (found) {
3208                    const char *id = xs_dict_get(msg, "id");
3209
3210                    if (op == NULL) {
3211                        /* no operation (?) */
3212                    }
3213                    else
3214                    if (strcmp(op, "favourite") == 0) { /** **/
3215                        xs *n_msg = msg_admiration(&snac, id, "Like");
3216
3217                        if (n_msg != NULL) {
3218                            enqueue_message(&snac, n_msg);
3219                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1);
3220
3221                            out = mastoapi_status(&snac, msg);
3222                        }
3223                    }
3224                    else
3225                    if (strcmp(op, "unfavourite") == 0) { /** **/
3226                        xs *n_msg = msg_repulsion(&snac, id, "Like");
3227
3228                        if (n_msg != NULL) {
3229                            enqueue_message(&snac, n_msg);
3230
3231                            out = mastoapi_status(&snac, msg);
3232                        }
3233                    }
3234                    else
3235                    if (strcmp(op, "reblog") == 0) { /** **/
3236                        xs *n_msg = msg_admiration(&snac, id, "Announce");
3237
3238                        if (n_msg != NULL) {
3239                            enqueue_message(&snac, n_msg);
3240                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0);
3241
3242                            out = mastoapi_status(&snac, msg);
3243                        }
3244                    }
3245                    else
3246                    if (strcmp(op, "unreblog") == 0) { /** **/
3247                        xs *n_msg = msg_repulsion(&snac, id, "Announce");
3248
3249                        if (n_msg != NULL) {
3250                            enqueue_message(&snac, n_msg);
3251
3252                            out = mastoapi_status(&snac, msg);
3253                        }
3254                    }
3255                    else
3256                    if (strcmp(op, "bookmark") == 0) { /** **/
3257                        /* bookmark this message */
3258                        if (bookmark(&snac, id) == 0)
3259                            out = mastoapi_status(&snac, msg);
3260                        else
3261                            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3262                    }
3263                    else
3264                    if (strcmp(op, "unbookmark") == 0) { /** **/
3265                        /* unbookmark this message */
3266                        unbookmark(&snac, id);
3267                        out = mastoapi_status(&snac, msg);
3268                    }
3269                    else
3270                    if (strcmp(op, "pin") == 0) { /** **/
3271                        /* pin this message */
3272                        if (pin(&snac, id) == 0)
3273                            out = mastoapi_status(&snac, msg);
3274                        else
3275                            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3276                    }
3277                    else
3278                    if (strcmp(op, "unpin") == 0) { /** **/
3279                        /* unpin this message */
3280                        unpin(&snac, id);
3281                        out = mastoapi_status(&snac, msg);
3282                    }
3283                    else
3284                    if (strcmp(op, "mute") == 0) { /** **/
3285                        /* Mastodon's mute is snac's hide */
3286                    }
3287                    else
3288                    if (strcmp(op, "unmute") == 0) { /** **/
3289                        /* Mastodon's unmute is snac's unhide */
3290                    }
3291                }
3292
3293                if (out != NULL) {
3294                    *body  = xs_json_dumps(out, 4);
3295                    *ctype = "application/json";
3296                    status = HTTP_STATUS_OK;
3297                }
3298            }
3299        }
3300        else
3301            status = HTTP_STATUS_UNAUTHORIZED;
3302    }
3303    else
3304    if (strcmp(cmd, "/v1/notifications/clear") == 0) { /** **/
3305        if (logged_in) {
3306            notify_clear(&snac);
3307            timeline_touch(&snac);
3308
3309            *body  = xs_dup("{}");
3310            *ctype = "application/json";
3311            status = HTTP_STATUS_OK;
3312        }
3313        else
3314            status = HTTP_STATUS_UNAUTHORIZED;
3315    }
3316    else
3317    if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
3318        /* I don't know what I'm doing */
3319        if (logged_in) {
3320            const char *v;
3321
3322            xs *wpush = xs_dict_new();
3323
3324            wpush = xs_dict_append(wpush, "id", "1");
3325
3326            v = xs_dict_get(args, "data");
3327            v = xs_dict_get(v, "alerts");
3328            wpush = xs_dict_append(wpush, "alerts", v);
3329
3330            v = xs_dict_get(args, "subscription");
3331            v = xs_dict_get(v, "endpoint");
3332            wpush = xs_dict_append(wpush, "endpoint", v);
3333
3334            xs *server_key = random_str();
3335            wpush = xs_dict_append(wpush, "server_key", server_key);
3336
3337            *body  = xs_json_dumps(wpush, 4);
3338            *ctype = "application/json";
3339            status = HTTP_STATUS_OK;
3340        }
3341        else
3342            status = HTTP_STATUS_UNAUTHORIZED;
3343    }
3344    else
3345    if (strcmp(cmd, "/v1/media") == 0 || strcmp(cmd, "/v2/media") == 0) { /** **/
3346        if (logged_in) {
3347            const xs_list *file = xs_dict_get(args, "file");
3348            const char *desc    = xs_dict_get(args, "description");
3349
3350            if (xs_is_null(desc))
3351                desc = "";
3352
3353            status = HTTP_STATUS_BAD_REQUEST;
3354
3355            if (xs_type(file) == XSTYPE_LIST) {
3356                const char *fn = xs_list_get(file, 0);
3357
3358                if (*fn != '\0') {
3359                    char *ext = strrchr(fn, '.');
3360                    char rnd[32];
3361                    xs_rnd_buf(rnd, sizeof(rnd));
3362                    xs *hash  = xs_md5_hex(rnd, sizeof(rnd));
3363                    xs *id    = xs_fmt("post-%s%s", hash, ext ? ext : "");
3364                    xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
3365                    int fo    = xs_number_get(xs_list_get(file, 1));
3366                    int fs    = xs_number_get(xs_list_get(file, 2));
3367
3368                    /* store */
3369                    static_put(&snac, id, payload + fo, fs);
3370                    static_put_meta(&snac, id, desc);
3371
3372                    /* prepare a response */
3373                    xs *rsp = xs_dict_new();
3374
3375                    rsp = xs_dict_append(rsp, "id",          id);
3376                    rsp = xs_dict_append(rsp, "type",        "image");
3377                    rsp = xs_dict_append(rsp, "url",         url);
3378                    rsp = xs_dict_append(rsp, "preview_url", url);
3379                    rsp = xs_dict_append(rsp, "remote_url",  url);
3380                    rsp = xs_dict_append(rsp, "description", desc);
3381
3382                    *body  = xs_json_dumps(rsp, 4);
3383                    *ctype = "application/json";
3384                    status = HTTP_STATUS_OK;
3385                }
3386            }
3387        }
3388        else
3389            status = HTTP_STATUS_UNAUTHORIZED;
3390    }
3391    else
3392    if (xs_startswith(cmd, "/v1/accounts")) { /** **/
3393        if (logged_in) {
3394            /* account-related information */
3395            xs *l = xs_split(cmd, "/");
3396            const char *md5 = xs_list_get(l, 3);
3397            const char *opt = xs_list_get(l, 4);
3398            xs *rsp = NULL;
3399
3400            if (!xs_is_null(md5) && *md5) {
3401                xs *actor_o = NULL;
3402
3403                if (xs_is_null(opt)) {
3404                    /* ? */
3405                }
3406                else
3407                if (strcmp(opt, "follow") == 0) { /** **/
3408                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
3409                        const char *actor = xs_dict_get(actor_o, "id");
3410
3411                        xs *msg = msg_follow(&snac, actor);
3412
3413                        if (msg != NULL) {
3414                            /* reload the actor from the message, in may be different */
3415                            actor = xs_dict_get(msg, "object");
3416
3417                            following_add(&snac, actor, msg);
3418
3419                            enqueue_output_by_actor(&snac, msg, actor, 0);
3420
3421                            rsp = mastoapi_relationship(&snac, md5);
3422                        }
3423                    }
3424                }
3425                else
3426                if (strcmp(opt, "unfollow") == 0) { /** **/
3427                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
3428                        const char *actor = xs_dict_get(actor_o, "id");
3429
3430                        /* get the following object */
3431                        xs *object = NULL;
3432
3433                        if (valid_status(following_get(&snac, actor, &object))) {
3434                            xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
3435
3436                            following_del(&snac, actor);
3437
3438                            enqueue_output_by_actor(&snac, msg, actor, 0);
3439
3440                            rsp = mastoapi_relationship(&snac, md5);
3441                        }
3442                    }
3443                }
3444                else
3445                if (strcmp(opt, "block") == 0) { /** **/
3446                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
3447                        const char *actor = xs_dict_get(actor_o, "id");
3448
3449                        mute(&snac, actor);
3450
3451                        rsp = mastoapi_relationship(&snac, md5);
3452                    }
3453                }
3454                else
3455                if (strcmp(opt, "unblock") == 0) { /** **/
3456                    if (valid_status(object_get_by_md5(md5, &actor_o))) {
3457                        const char *actor = xs_dict_get(actor_o, "id");
3458
3459                        unmute(&snac, actor);
3460
3461                        rsp = mastoapi_relationship(&snac, md5);
3462                    }
3463                }
3464            }
3465
3466            if (rsp != NULL) {
3467                *body  = xs_json_dumps(rsp, 4);
3468                *ctype = "application/json";
3469                status = HTTP_STATUS_OK;
3470            }
3471        }
3472        else
3473            status = HTTP_STATUS_UNAUTHORIZED;
3474    }
3475    else
3476    if (xs_startswith(cmd, "/v1/polls")) { /** **/
3477        if (logged_in) {
3478            /* operations on a status */
3479            xs *l = xs_split(cmd, "/");
3480            const char *mid = xs_list_get(l, 3);
3481            const char *op  = xs_list_get(l, 4);
3482
3483            if (!xs_is_null(mid)) {
3484                xs *msg = NULL;
3485                xs *out = NULL;
3486
3487                /* skip the 'fake' part of the id */
3488                mid = MID_TO_MD5(mid);
3489
3490                if (valid_status(timeline_get_by_md5(&snac, mid, &msg))) {
3491                    const char *id   = xs_dict_get(msg, "id");
3492                    const char *atto = get_atto(msg);
3493
3494                    const xs_list *opts = xs_dict_get(msg, "oneOf");
3495                    if (opts == NULL)
3496                        opts = xs_dict_get(msg, "anyOf");
3497
3498                    if (op == NULL) {
3499                    }
3500                    else
3501                    if (strcmp(op, "votes") == 0) {
3502                        const xs_list *choices = xs_dict_get(args, "choices[]");
3503
3504                        if (xs_is_null(choices))
3505                            choices = xs_dict_get(args, "choices");
3506
3507                        if (xs_type(choices) == XSTYPE_LIST) {
3508                            const xs_str *v;
3509
3510                            int c = 0;
3511                            while (xs_list_next(choices, &v, &c)) {
3512                                int io           = atoi(v);
3513                                const xs_dict *o = xs_list_get(opts, io);
3514
3515                                if (o) {
3516                                    const char *name = xs_dict_get(o, "name");
3517
3518                                    xs *msg = msg_note(&snac, "", atto, (char *)id, NULL, 1, NULL, NULL);
3519                                    msg = xs_dict_append(msg, "name", name);
3520
3521                                    xs *c_msg = msg_create(&snac, msg);
3522                                    enqueue_message(&snac, c_msg);
3523                                    timeline_add(&snac, xs_dict_get(msg, "id"), msg);
3524                                }
3525                            }
3526
3527                            out = mastoapi_poll(&snac, msg);
3528                        }
3529                    }
3530                }
3531
3532                if (out != NULL) {
3533                    *body  = xs_json_dumps(out, 4);
3534                    *ctype = "application/json";
3535                    status = HTTP_STATUS_OK;
3536                }
3537            }
3538        }
3539        else
3540            status = HTTP_STATUS_UNAUTHORIZED;
3541    }
3542    else
3543    if (strcmp(cmd, "/v1/lists") == 0) {
3544        if (logged_in) {
3545            const char *title = xs_dict_get(args, "title");
3546
3547            if (xs_type(title) == XSTYPE_STRING) {
3548                /* add the list */
3549                xs *out = xs_dict_new();
3550                xs *lid = list_maint(&snac, title, 1);
3551
3552                if (!xs_is_null(lid)) {
3553                    out = xs_dict_append(out, "id", lid);
3554                    out = xs_dict_append(out, "title", title);
3555                    out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
3556                    out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
3557
3558                    status = HTTP_STATUS_OK;
3559                }
3560                else {
3561                    out = xs_dict_append(out, "error", "cannot create list");
3562                    status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3563                }
3564
3565                *body  = xs_json_dumps(out, 4);
3566                *ctype = "application/json";
3567            }
3568            else
3569                status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3570        }
3571    }
3572    else
3573    if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
3574        if (logged_in) {
3575            xs *l = xs_split(cmd, "/");
3576            const char *op = xs_list_get(l, -1);
3577            const char *id = xs_list_get(l, -2);
3578
3579            if (op && id && xs_is_hex(id)) {
3580                if (strcmp(op, "accounts") == 0) {
3581                    const xs_list *accts = xs_dict_get(args, "account_ids[]");
3582
3583                    if (xs_is_null(accts))
3584                        accts = xs_dict_get(args, "account_ids");
3585
3586                    int c = 0;
3587                    const char *v;
3588
3589                    while (xs_list_next(accts, &v, &c)) {
3590                        list_members(&snac, id, v, 1);
3591                    }
3592
3593                    xs *out = xs_dict_new();
3594                    *body   = xs_json_dumps(out, 4);
3595                    *ctype  = "application/json";
3596                    status  = HTTP_STATUS_OK;
3597                }
3598            }
3599        }
3600    }
3601    else if (strcmp(cmd, "/v1/markers") == 0) { /** **/
3602        xs_str *json = NULL;
3603        if (logged_in) {
3604            const xs_str *home_marker = xs_dict_get(args, "home[last_read_id]");
3605            if (xs_is_null(home_marker)) {
3606                const xs_dict *home = xs_dict_get(args, "home");
3607                if (!xs_is_null(home))
3608                    home_marker = xs_dict_get(home, "last_read_id");
3609            }
3610
3611            const xs_str *notify_marker = xs_dict_get(args, "notifications[last_read_id]");
3612            if (xs_is_null(notify_marker)) {
3613                const xs_dict *notify = xs_dict_get(args, "notifications");
3614                if (!xs_is_null(notify))
3615                    notify_marker = xs_dict_get(notify, "last_read_id");
3616            }
3617            json = xs_json_dumps(markers_set(&snac, home_marker, notify_marker), 4);
3618        }
3619        if (!xs_is_null(json))
3620            *body = json;
3621        else
3622            *body = xs_dup("{}");
3623
3624        *ctype = "application/json";
3625        status = HTTP_STATUS_OK;
3626    }
3627    else
3628    if (xs_startswith(cmd, "/v1/follow_requests")) { /** **/
3629        if (logged_in) {
3630            /* "authorize" or "reject" */
3631            xs *rel = NULL;
3632            xs *l = xs_split(cmd, "/");
3633            const char *md5 = xs_list_get(l, -2);
3634            const char *s_cmd = xs_list_get(l, -1);
3635
3636            if (xs_is_string(md5) && xs_is_string(s_cmd)) {
3637                xs *actor = NULL;
3638
3639                if (valid_status(object_get_by_md5(md5, &actor))) {
3640                    const char *actor_id = xs_dict_get(actor, "id");
3641
3642                    if (strcmp(s_cmd, "authorize") == 0) {
3643                        xs *fwreq = pending_get(&snac, actor_id);
3644
3645                        if (fwreq != NULL) {
3646                            xs *reply = msg_accept(&snac, fwreq, actor_id);
3647
3648                            enqueue_message(&snac, reply);
3649
3650                            if (xs_is_null(xs_dict_get(fwreq, "published"))) {
3651                                xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3652                                fwreq = xs_dict_set(fwreq, "published", date);
3653                            }
3654
3655                            timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
3656
3657                            follower_add(&snac, actor_id);
3658
3659                            pending_del(&snac, actor_id);
3660                            rel = mastoapi_relationship(&snac, md5);
3661                        }
3662                    }
3663                    else
3664                    if (strcmp(s_cmd, "reject") == 0) {
3665                        pending_del(&snac, actor_id);
3666                        rel = mastoapi_relationship(&snac, md5);
3667                    }
3668                }
3669            }
3670
3671            if (rel != NULL) {
3672                *body = xs_json_dumps(rel, 4);
3673                *ctype = "application/json";
3674                status = HTTP_STATUS_OK;
3675            }
3676        }
3677    }
3678    else
3679    if (xs_startswith(cmd, "/v1/tags/")) { /** **/
3680        if (logged_in) {
3681            xs *l = xs_split(cmd, "/");
3682            const char *i_tag = xs_list_get(l, 3);
3683            const char *cmd = xs_list_get(l, 4);
3684
3685            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3686
3687            if (xs_is_string(i_tag) && xs_is_string(cmd)) {
3688                int ok = 0;
3689
3690                xs *tag = xs_fmt("#%s", i_tag);
3691                xs *followed_hashtags = xs_dup(xs_dict_get_def(snac.config,
3692                                "followed_hashtags", xs_stock(XSTYPE_LIST)));
3693
3694                if (strcmp(cmd, "follow") == 0) {
3695                    followed_hashtags = xs_list_append(followed_hashtags, tag);
3696                    ok = 1;
3697                }
3698                else
3699                if (strcmp(cmd, "unfollow") == 0) {
3700                    int off = xs_list_in(followed_hashtags, tag);
3701
3702                    if (off != -1)
3703                        followed_hashtags = xs_list_del(followed_hashtags, off);
3704
3705                    ok = 1;
3706                }
3707
3708                if (ok) {
3709                    /* update */
3710                    xs_dict_set(snac.config, "followed_hashtags", followed_hashtags);
3711                    user_persist(&snac, 0);
3712
3713                    xs *d = xs_dict_new();
3714                    xs *s = xs_fmt("%s?t=%s", srv_baseurl, i_tag);
3715                    d = xs_dict_set(d, "name", i_tag);
3716                    d = xs_dict_set(d, "url", s);
3717                    d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST));
3718
3719                    *body = xs_json_dumps(d, 4);
3720                    *ctype = "application/json";
3721                    status = HTTP_STATUS_OK;
3722                }
3723            }
3724        }
3725    }
3726    else
3727        status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3728
3729    /* user cleanup */
3730    if (logged_in)
3731        user_free(&snac);
3732
3733    srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status));
3734
3735    return status;
3736}
3737
3738
3739int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
3740                          const char *payload, int p_size,
3741                          char **body, int *b_size, char **ctype)
3742{
3743    (void)p_size;
3744    (void)b_size;
3745    (void)ctype;
3746
3747    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
3748        return 0;
3749
3750    int status    = HTTP_STATUS_NOT_FOUND;
3751    xs *args      = NULL;
3752    const char *i_ctype = xs_dict_get(req, "content-type");
3753
3754    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
3755        if (!xs_is_null(payload))
3756            args = xs_json_loads(payload);
3757    }
3758    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
3759    {
3760        // Some apps send form data instead of json so we should cater for those
3761        if (!xs_is_null(payload)) {
3762            args = xs_url_vars(payload);
3763        }
3764    }
3765    else
3766        args = xs_dup(xs_dict_get(req, "p_vars"));
3767
3768    if (args == NULL)
3769        return HTTP_STATUS_BAD_REQUEST;
3770
3771    snac snac = {0};
3772    int logged_in = process_auth_token(&snac, req);
3773
3774    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
3775
3776    if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
3777        // pretend we deleted it, since it doesn't exist anyway
3778        status = HTTP_STATUS_OK;
3779    }
3780    else
3781    if (xs_startswith(cmd, "/v1/lists/")) {
3782        if (logged_in) {
3783            xs *l = xs_split(cmd, "/");
3784            const char *p = xs_list_get(l, -1);
3785
3786            if (p) {
3787                if (strcmp(p, "accounts") == 0) {
3788                    /* delete account from list */
3789                    p = xs_list_get(l, -2);
3790                    const xs_list *accts = xs_dict_get(args, "account_ids[]");
3791
3792                    if (xs_is_null(accts))
3793                        accts = xs_dict_get(args, "account_ids");
3794
3795                    int c = 0;
3796                    const char *v;
3797
3798                    while (xs_list_next(accts, &v, &c)) {
3799                        list_members(&snac, p, v, 2);
3800                    }
3801                }
3802                else {
3803                    /* delete list */
3804                    if (xs_is_hex(p)) {
3805                        list_maint(&snac, p, 2);
3806                    }
3807                }
3808            }
3809
3810            *ctype = "application/json";
3811            status = HTTP_STATUS_OK;
3812        }
3813        else
3814            status = HTTP_STATUS_UNAUTHORIZED;
3815    }
3816    else
3817    if (xs_startswith(cmd, "/v1/statuses/")) {
3818        if (logged_in) {
3819            xs *l = xs_split(cmd, "/");
3820            const char *p = xs_list_get(l, -1);
3821            p = MID_TO_MD5(p);
3822
3823            xs *obj = NULL;
3824            if (valid_status(object_get_by_md5(p, &obj))) {
3825                const char *id = xs_dict_get(obj, "id");
3826
3827                if (xs_is_string(id) && is_msg_mine(&snac, id)) {
3828                    xs *out = mastoapi_status(&snac, obj);
3829
3830                    xs *msg = msg_delete(&snac, id);
3831
3832                    enqueue_message(&snac, msg);
3833
3834                    timeline_del(&snac, id);
3835
3836                    draft_del(&snac, id);
3837
3838                    schedule_del(&snac, id);
3839
3840                    snac_log(&snac, xs_fmt("deleted entry %s", id));
3841
3842                    *body = xs_json_dumps(out, 4);
3843                    *ctype = "application/json";
3844                    status = HTTP_STATUS_OK;
3845                }
3846            }
3847        }
3848        else
3849            status = HTTP_STATUS_UNAUTHORIZED;
3850    }
3851
3852    /* user cleanup */
3853    if (logged_in)
3854        user_free(&snac);
3855
3856    srv_debug(1, xs_fmt("mastoapi_delete_handler %s %d", q_path, status));
3857
3858    return status;
3859}
3860
3861
3862int mastoapi_put_handler(const xs_dict *req, const char *q_path,
3863                          const char *payload, int p_size,
3864                          char **body, int *b_size, char **ctype)
3865{
3866    (void)p_size;
3867    (void)b_size;
3868
3869    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
3870        return 0;
3871
3872    int status    = HTTP_STATUS_NOT_FOUND;
3873    xs *args      = NULL;
3874    const char *i_ctype = xs_dict_get(req, "content-type");
3875
3876    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
3877        if (!xs_is_null(payload))
3878            args = xs_json_loads(payload);
3879    }
3880    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
3881    {
3882        // Some apps send form data instead of json so we should cater for those
3883        if (!xs_is_null(payload)) {
3884            args    = xs_url_vars(payload);
3885        }
3886    }
3887    else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data"))
3888    {
3889        // Handle multipart/form-data by using p_vars (already parsed by httpd)
3890        args = xs_dup(xs_dict_get(req, "p_vars"));
3891    }
3892
3893    /* if args still NULL, try falling back to p_vars or q_vars */
3894    if (args == NULL)
3895        args = xs_dup(xs_dict_get(req, "p_vars"));
3896
3897    if (args == NULL)
3898        args = xs_dup(xs_dict_get(req, "q_vars"));
3899
3900    if (args == NULL) {
3901        srv_debug(1, xs_fmt("mastoapi_put_handler: failed to parse args for %s, content-type: %s",
3902                           q_path, i_ctype ? i_ctype : "(null)"));
3903        return HTTP_STATUS_BAD_REQUEST;
3904    }
3905
3906    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
3907
3908    snac snac = {0};
3909    int logged_in = process_auth_token(&snac, req);
3910
3911    if (xs_startswith(cmd, "/v1/media") || xs_startswith(cmd, "/v2/media")) { /** **/
3912        if (logged_in) {
3913            xs *l = xs_split(cmd, "/");
3914            const char *stid = xs_list_get(l, 3);
3915
3916            if (!xs_is_null(stid)) {
3917                const char *desc = xs_dict_get(args, "description");
3918
3919                /* set the image metadata */
3920                static_put_meta(&snac, stid, desc);
3921
3922                /* prepare a response */
3923                xs *rsp = xs_dict_new();
3924                xs *url = xs_fmt("%s/s/%s", snac.actor, stid);
3925
3926                rsp = xs_dict_append(rsp, "id",          stid);
3927                rsp = xs_dict_append(rsp, "type",        "image");
3928                rsp = xs_dict_append(rsp, "url",         url);
3929                rsp = xs_dict_append(rsp, "preview_url", url);
3930                rsp = xs_dict_append(rsp, "remote_url",  url);
3931                rsp = xs_dict_append(rsp, "description", desc);
3932
3933                *body  = xs_json_dumps(rsp, 4);
3934                *ctype = "application/json";
3935                status = HTTP_STATUS_OK;
3936            }
3937        }
3938        else
3939            status = HTTP_STATUS_UNAUTHORIZED;
3940    }
3941    else
3942    if (xs_startswith(cmd, "/v1/statuses")) {
3943        if (logged_in) {
3944            xs *l = xs_split(cmd, "/");
3945            const char *mid = xs_list_get(l, 3);
3946
3947            if (!xs_is_null(mid)) {
3948                const char *md5 = MID_TO_MD5(mid);
3949                xs *rsp = NULL;
3950                xs *msg = NULL;
3951
3952                if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) {
3953                    const char *content = xs_dict_get(args, "status");
3954                    xs *atls = xs_list_new();
3955                    xs *f_content = not_really_markdown(content, &atls, NULL);
3956
3957                    /* replace fields with new content */
3958                    msg = xs_dict_set(msg, "sourceContent", content);
3959                    msg = xs_dict_set(msg, "content", f_content);
3960
3961                    xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
3962                    msg = xs_dict_set(msg, "updated", updated);
3963
3964                    /* overwrite object, not updating the indexes */
3965                    object_add_ow(xs_dict_get(msg, "id"), msg);
3966
3967                    /* update message */
3968                    xs *c_msg = msg_update(&snac, msg);
3969
3970                    enqueue_message(&snac, c_msg);
3971
3972                    rsp = mastoapi_status(&snac, msg);
3973                }
3974
3975                if (rsp != NULL) {
3976                    *body  = xs_json_dumps(rsp, 4);
3977                    *ctype = "application/json";
3978                    status = HTTP_STATUS_OK;
3979                }
3980            }
3981        }
3982        else
3983            status = HTTP_STATUS_UNAUTHORIZED;
3984    }
3985
3986    /* user cleanup */
3987    if (logged_in)
3988        user_free(&snac);
3989
3990    srv_debug(1, xs_fmt("mastoapi_put_handler %s %d", q_path, status));
3991
3992    return status;
3993}
3994
3995void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac)
3996/* Store header or avatar */
3997{
3998    if (data != NULL) {
3999        if (xs_type(data) == XSTYPE_LIST) {
4000            const char *fn = xs_list_get(data, 0);
4001
4002            if (fn && *fn) {
4003                const char *ext = strrchr(fn, '.');
4004
4005                /* Mona iOS sends always jpg as application/octet-stream with no filename */
4006                if (ext == NULL || strcmp(fn, key) == 0) {
4007                    ext = ".jpg";
4008                }
4009
4010                /* Make sure we have a unique file name, otherwise updated images will not be
4011                 * re-loaded by clients. */
4012                xs *rnd         = random_str();
4013                xs *hash        = xs_md5_hex(rnd, strlen(rnd));
4014                xs *id          = xs_fmt("%s%s", hash, ext);
4015                xs *url         = xs_fmt("%s/s/%s", snac->actor, id);
4016                int fo          = xs_number_get(xs_list_get(data, 1));
4017                int fs          = xs_number_get(xs_list_get(data, 2));
4018
4019                /* store */
4020                static_put(snac, id, payload + fo, fs);
4021
4022                snac->config = xs_dict_set(snac->config, key, url);
4023            }
4024        }
4025    }
4026}
4027
4028int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
4029                          const char *payload, int p_size,
4030                          char **body, int *b_size, char **ctype)
4031/* Handle profile updates */
4032{
4033    (void)p_size;
4034    (void)b_size;
4035
4036    if (!xs_startswith(q_path, "/api/v1/"))
4037        return 0;
4038
4039    int status    = HTTP_STATUS_NOT_FOUND;
4040    xs *args      = NULL;
4041    const char *i_ctype = xs_dict_get(req, "content-type");
4042
4043    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
4044        if (!xs_is_null(payload))
4045            args = xs_json_loads(payload);
4046    }
4047    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
4048    {
4049        // Some apps send form data instead of json so we should cater for those
4050        if (!xs_is_null(payload)) {
4051            args    = xs_url_vars(payload);
4052        }
4053    }
4054    else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data"))
4055    {
4056        // Handle multipart/form-data by using p_vars (already parsed by httpd)
4057        args = xs_dup(xs_dict_get(req, "p_vars"));
4058    }
4059
4060    /* if args still NULL, try falling back to p_vars or q_vars */
4061    if (args == NULL)
4062        args = xs_dup(xs_dict_get(req, "p_vars"));
4063
4064    if (args == NULL)
4065        args = xs_dup(xs_dict_get(req, "q_vars"));
4066
4067    if (args == NULL) {
4068        srv_debug(1, xs_fmt("mastoapi_patch_handler: failed to parse args for %s, content-type: %s",
4069                           q_path, i_ctype ? i_ctype : "(null)"));
4070        return HTTP_STATUS_BAD_REQUEST;
4071    }
4072
4073    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
4074
4075    snac snac = {0};
4076    int logged_in = process_auth_token(&snac, req);
4077
4078    if (xs_startswith(cmd, "/v1/accounts/update_credentials")) { /** **/
4079        /* Update user profile fields */
4080        if (logged_in) {
4081            int c = 0;
4082            const xs_str *k;
4083            const xs_val *v;
4084            const xs_str *field_name = NULL;
4085            xs *new_fields = xs_dict_new();
4086            while (xs_dict_next(args, &k, &v, &c)) {
4087                if (strcmp(k, "display_name") == 0) {
4088                    if (v != NULL)
4089                        snac.config = xs_dict_set(snac.config, "name", v);
4090                }
4091                else
4092                if (strcmp(k, "note") == 0) {
4093                    if (v != NULL)
4094                        snac.config = xs_dict_set(snac.config, "bio", v);
4095                }
4096                else
4097                if (strcmp(k, "bot") == 0) {
4098                    if (v != NULL)
4099                        snac.config = xs_dict_set(snac.config, "bot",
4100                            (strcmp(v, "true") == 0 ||
4101                                strcmp(v, "1") == 0) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
4102                }
4103                else
4104                if (strcmp(k, "source[sensitive]") == 0) {
4105                    if (v != NULL)
4106                        snac.config = xs_dict_set(snac.config, "cw",
4107                            strcmp(v, "true") == 0 ? "open" : "");
4108                }
4109                else
4110                if (strcmp(k, "source[privacy]") == 0) {
4111                    if (v != NULL)
4112                        snac.config = xs_dict_set(snac.config, "private",
4113                            strcmp(v, "private") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
4114                }
4115                else
4116                if (strcmp(k, "header") == 0) {
4117                    persist_image("header", v, payload, &snac);
4118                }
4119                else
4120                if (strcmp(k, "avatar") == 0) {
4121                    persist_image("avatar", v, payload, &snac);
4122                }
4123                else
4124                if (xs_between("fields_attributes", k, "[name]")) {
4125                    field_name = strcmp(v, "") != 0 ? v : NULL;
4126                }
4127                else
4128                if (xs_between("fields_attributes", k, "[value]")) {
4129                    if (field_name != NULL) {
4130                        new_fields = xs_dict_set(new_fields, field_name, v);
4131                        snac.config = xs_dict_set(snac.config, "metadata", new_fields);
4132                    }
4133                }
4134                else
4135                if (strcmp(k, "locked") == 0) {
4136                    snac.config = xs_dict_set(snac.config, "approve_followers",
4137                        xs_stock(strcmp(v, "true") == 0 ? XSTYPE_TRUE : XSTYPE_FALSE));
4138                }
4139                /* we don't have support for the following options, yet
4140                   - discoverable (0/1)
4141                 */
4142            }
4143
4144            /* Persist profile */
4145            if (user_persist(&snac, 1) == 0)
4146                credentials_get(body, ctype, &status, snac);
4147            else
4148                status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
4149        }
4150        else
4151            status = HTTP_STATUS_UNAUTHORIZED;
4152    }
4153
4154    /* user cleanup */
4155    if (logged_in)
4156        user_free(&snac);
4157
4158    srv_debug(1, xs_fmt("mastoapi_patch_handler %s %d", q_path, status));
4159
4160    return status;
4161}
4162
4163
4164void mastoapi_purge(void)
4165{
4166    xs *spec   = xs_fmt("%s/app/" "*.json", srv_basedir);
4167    xs *files  = xs_glob(spec, 1, 0);
4168    xs_list *p = files;
4169    const xs_str *v;
4170
4171    time_t mt = time(NULL) - 3600;
4172
4173    while (xs_list_iter(&p, &v)) {
4174        xs *cid = xs_replace(v, ".json", "");
4175        xs *fn  = _app_fn(cid);
4176
4177        if (mtime(fn) < mt) {
4178            /* get the app */
4179            xs *app = app_get(cid);
4180
4181            if (app) {
4182                /* old apps with no uid are incomplete cruft */
4183                const char *uid = xs_dict_get(app, "uid");
4184
4185                if (xs_is_null(uid) || *uid == '\0') {
4186                    unlink(fn);
4187                    srv_debug(2, xs_fmt("purged %s", fn));
4188                }
4189            }
4190        }
4191    }
4192}
4193
4194
4195#endif /* #ifndef NO_MASTODON_API */