snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

mastoapi.c (126799B)


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