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