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 */