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