2.53
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 *timeline = timeline_simple_list(&snac1, "private", 0, 2048);
1471
1472 xs *out = xs_list_new();
1473 xs_list *p = timeline;
1474 xs_str *v;
1475
1476 while (xs_list_iter(&p, &v) && cnt < limit) {
1477 xs *msg = NULL;
1478
1479 /* only return entries older that max_id */
1480 if (max_id) {
1481 if (strcmp(v, MID_TO_MD5(max_id)) == 0)
1482 max_id = NULL;
1483
1484 continue;
1485 }
1486
1487 /* only returns entries newer than since_id */
1488 if (since_id) {
1489 if (strcmp(v, MID_TO_MD5(since_id)) == 0)
1490 break;
1491 }
1492
1493 /* only returns entries newer than min_id */
1494 /* what does really "Return results immediately newer than ID" mean? */
1495 if (min_id) {
1496 if (strcmp(v, MID_TO_MD5(min_id)) == 0)
1497 break;
1498 }
1499
1500 /* get the entry */
1501 if (!valid_status(timeline_get_by_md5(&snac1, v, &msg)))
1502 continue;
1503
1504 /* discard non-Notes */
1505 const char *id = xs_dict_get(msg, "id");
1506 const char *type = xs_dict_get(msg, "type");
1507 if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
1508 continue;
1509
1510 const char *from = NULL;
1511 if (strcmp(type, "Page") == 0)
1512 from = xs_dict_get(msg, "audience");
1513
1514 if (from == NULL)
1515 from = get_atto(msg);
1516
1517 if (from == NULL)
1518 continue;
1519
1520 /* is this message from a person we don't follow? */
1521 if (strcmp(from, snac1.actor) && !following_check(&snac1, from)) {
1522 /* discard if it was not boosted */
1523 xs *idx = object_announces(id);
1524
1525 if (xs_list_len(idx) == 0)
1526 continue;
1527 }
1528
1529 /* discard notes from muted morons */
1530 if (is_muted(&snac1, from))
1531 continue;
1532
1533 /* discard hidden notes */
1534 if (is_hidden(&snac1, id))
1535 continue;
1536
1537 /* if it has a name and it's not a Page or a Video,
1538 it's a poll vote, so discard it */
1539 if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video"))
1540 continue;
1541
1542 /* convert the Note into a Mastodon status */
1543 xs *st = mastoapi_status(&snac1, msg);
1544
1545 if (st != NULL)
1546 out = xs_list_append(out, st);
1547
1548 cnt++;
1549 }
1550
1551 *body = xs_json_dumps(out, 4);
1552 *ctype = "application/json";
1553 status = 200;
1554
1555 srv_debug(2, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out)));
1556 }
1557 else {
1558 status = 401; // unauthorized
1559 }
1560 }
1561 else
1562 if (strcmp(cmd, "/v1/timelines/public") == 0) { /** **/
1563 /* the instance public timeline (public timelines for all users) */
1564
1565 const char *limit_s = xs_dict_get(args, "limit");
1566 int limit = 0;
1567 int cnt = 0;
1568
1569 if (!xs_is_null(limit_s))
1570 limit = atoi(limit_s);
1571
1572 if (limit == 0)
1573 limit = 20;
1574
1575 xs *timeline = timeline_instance_list(0, limit);
1576 xs *out = xs_list_new();
1577 xs_list *p = timeline;
1578 xs_str *md5;
1579
1580 snac *user = NULL;
1581 if (logged_in)
1582 user = &snac1;
1583
1584 while (xs_list_iter(&p, &md5) && cnt < limit) {
1585 xs *msg = NULL;
1586
1587 /* get the entry */
1588 if (!valid_status(object_get_by_md5(md5, &msg)))
1589 continue;
1590
1591 /* discard non-Notes */
1592 const char *type = xs_dict_get(msg, "type");
1593 if (strcmp(type, "Note") != 0 && strcmp(type, "Question") != 0)
1594 continue;
1595
1596 /* discard messages from private users */
1597 if (is_msg_from_private_user(msg))
1598 continue;
1599
1600 /* convert the Note into a Mastodon status */
1601 xs *st = mastoapi_status(user, msg);
1602
1603 if (st != NULL) {
1604 out = xs_list_append(out, st);
1605 cnt++;
1606 }
1607 }
1608
1609 *body = xs_json_dumps(out, 4);
1610 *ctype = "application/json";
1611 status = 200;
1612 }
1613 else
1614 if (xs_startswith(cmd, "/v1/timelines/tag/")) { /** **/
1615 const char *limit_s = xs_dict_get(args, "limit");
1616 int limit = 0;
1617 int cnt = 0;
1618
1619 if (!xs_is_null(limit_s))
1620 limit = atoi(limit_s);
1621
1622 if (limit == 0)
1623 limit = 20;
1624
1625 /* get the tag */
1626 xs *l = xs_split(cmd, "/");
1627 char *tag = xs_list_get(l, -1);
1628
1629 xs *timeline = tag_search(tag, 0, limit);
1630 xs *out = xs_list_new();
1631 xs_list *p = timeline;
1632 xs_str *md5;
1633
1634 while (xs_list_iter(&p, &md5) && cnt < limit) {
1635 xs *msg = NULL;
1636
1637 /* get the entry */
1638 if (!valid_status(object_get_by_md5(md5, &msg)))
1639 continue;
1640
1641 /* skip non-public messages */
1642 if (!is_msg_public(msg))
1643 continue;
1644
1645 /* discard messages from private users */
1646 if (is_msg_from_private_user(msg))
1647 continue;
1648
1649 /* convert the Note into a Mastodon status */
1650 xs *st = mastoapi_status(NULL, msg);
1651
1652 if (st != NULL) {
1653 out = xs_list_append(out, st);
1654 cnt++;
1655 }
1656 }
1657
1658 *body = xs_json_dumps(out, 4);
1659 *ctype = "application/json";
1660 status = 200;
1661 }
1662 else
1663 if (xs_startswith(cmd, "/v1/timelines/list/")) { /** **/
1664 /* get the list id */
1665 if (logged_in) {
1666 xs *l = xs_split(cmd, "/");
1667 char *list = xs_list_get(l, -1);
1668
1669 xs *timeline = list_timeline(&snac1, list, 0, 2048);
1670 xs *out = xs_list_new();
1671 int c = 0;
1672 char *md5;
1673
1674 while (xs_list_next(timeline, &md5, &c)) {
1675 xs *msg = NULL;
1676
1677 /* get the entry */
1678 if (!valid_status(timeline_get_by_md5(&snac1, md5, &msg)))
1679 continue;
1680
1681 /* discard non-Notes */
1682 const char *id = xs_dict_get(msg, "id");
1683 const char *type = xs_dict_get(msg, "type");
1684 if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
1685 continue;
1686
1687 const char *from = NULL;
1688 if (strcmp(type, "Page") == 0)
1689 from = xs_dict_get(msg, "audience");
1690
1691 if (from == NULL)
1692 from = get_atto(msg);
1693
1694 if (from == NULL)
1695 continue;
1696
1697 /* is this message from a person we don't follow? */
1698 if (strcmp(from, snac1.actor) && !following_check(&snac1, from)) {
1699 /* discard if it was not boosted */
1700 xs *idx = object_announces(id);
1701
1702 if (xs_list_len(idx) == 0)
1703 continue;
1704 }
1705
1706 /* discard notes from muted morons */
1707 if (is_muted(&snac1, from))
1708 continue;
1709
1710 /* discard hidden notes */
1711 if (is_hidden(&snac1, id))
1712 continue;
1713
1714 /* if it has a name and it's not a Page or a Video,
1715 it's a poll vote, so discard it */
1716 if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video"))
1717 continue;
1718
1719 /* convert the Note into a Mastodon status */
1720 xs *st = mastoapi_status(&snac1, msg);
1721
1722 if (st != NULL)
1723 out = xs_list_append(out, st);
1724 }
1725
1726 *body = xs_json_dumps(out, 4);
1727 *ctype = "application/json";
1728 status = 200;
1729 }
1730 else
1731 status = 421;
1732 }
1733 else
1734 if (strcmp(cmd, "/v1/conversations") == 0) { /** **/
1735 /* TBD */
1736 *body = xs_dup("[]");
1737 *ctype = "application/json";
1738 status = 200;
1739 }
1740 else
1741 if (strcmp(cmd, "/v1/notifications") == 0) { /** **/
1742 if (logged_in) {
1743 xs *l = notify_list(&snac1, 0, 64);
1744 xs *out = xs_list_new();
1745 xs_list *p = l;
1746 xs_dict *v;
1747 xs_list *excl = xs_dict_get(args, "exclude_types[]");
1748
1749 while (xs_list_iter(&p, &v)) {
1750 xs *noti = notify_get(&snac1, v);
1751
1752 if (noti == NULL)
1753 continue;
1754
1755 const char *type = xs_dict_get(noti, "type");
1756 const char *utype = xs_dict_get(noti, "utype");
1757 const char *objid = xs_dict_get(noti, "objid");
1758 xs *actor = NULL;
1759 xs *entry = NULL;
1760
1761 if (!valid_status(actor_get(xs_dict_get(noti, "actor"), &actor)))
1762 continue;
1763
1764 if (objid != NULL && !valid_status(object_get(objid, &entry)))
1765 continue;
1766
1767 if (is_hidden(&snac1, objid))
1768 continue;
1769
1770 /* convert the type */
1771 if (strcmp(type, "Like") == 0)
1772 type = "favourite";
1773 else
1774 if (strcmp(type, "Announce") == 0)
1775 type = "reblog";
1776 else
1777 if (strcmp(type, "Follow") == 0)
1778 type = "follow";
1779 else
1780 if (strcmp(type, "Create") == 0)
1781 type = "mention";
1782 else
1783 if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
1784 type = "poll";
1785 else
1786 continue;
1787
1788 /* excluded type? */
1789 if (!xs_is_null(excl) && xs_list_in(excl, type) != -1)
1790 continue;
1791
1792 xs *mn = xs_dict_new();
1793
1794 mn = xs_dict_append(mn, "type", type);
1795
1796 xs *id = xs_replace(xs_dict_get(noti, "id"), ".", "");
1797 mn = xs_dict_append(mn, "id", id);
1798
1799 mn = xs_dict_append(mn, "created_at", xs_dict_get(noti, "date"));
1800
1801 xs *acct = mastoapi_account(actor);
1802 mn = xs_dict_append(mn, "account", acct);
1803
1804 if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) {
1805 xs *st = mastoapi_status(&snac1, entry);
1806
1807 if (st)
1808 mn = xs_dict_append(mn, "status", st);
1809 }
1810
1811 out = xs_list_append(out, mn);
1812 }
1813
1814 *body = xs_json_dumps(out, 4);
1815 *ctype = "application/json";
1816 status = 200;
1817 }
1818 else
1819 status = 401;
1820 }
1821 else
1822 if (strcmp(cmd, "/v1/filters") == 0) { /** **/
1823 /* snac will never have filters */
1824 *body = xs_dup("[]");
1825 *ctype = "application/json";
1826 status = 200;
1827 }
1828 else
1829 if (strcmp(cmd, "/v2/filters") == 0) { /** **/
1830 /* snac will never have filters
1831 * but still, without a v2 endpoint a short delay is introduced
1832 * in some apps */
1833 *body = xs_dup("[]");
1834 *ctype = "application/json";
1835 status = 200;
1836 }
1837 else
1838 if (strcmp(cmd, "/v1/favourites") == 0) { /** **/
1839 /* snac will never support a list of favourites */
1840 *body = xs_dup("[]");
1841 *ctype = "application/json";
1842 status = 200;
1843 }
1844 else
1845 if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/
1846 /* snac does not support bookmarks */
1847 *body = xs_dup("[]");
1848 *ctype = "application/json";
1849 status = 200;
1850 }
1851 else
1852 if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
1853 if (logged_in) {
1854 xs *lol = list_maint(&snac1, NULL, 0);
1855 xs *l = xs_list_new();
1856 int c = 0;
1857 xs_list *li;
1858
1859 while (xs_list_next(lol, &li, &c)) {
1860 xs *d = xs_dict_new();
1861
1862 d = xs_dict_append(d, "id", xs_list_get(li, 0));
1863 d = xs_dict_append(d, "title", xs_list_get(li, 1));
1864 d = xs_dict_append(d, "replies_policy", "list");
1865 d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
1866
1867 l = xs_list_append(l, d);
1868 }
1869
1870 *body = xs_json_dumps(l, 4);
1871 *ctype = "application/json";
1872 status = 200;
1873 }
1874 }
1875 else
1876 if (xs_startswith(cmd, "/v1/lists/")) { /** list information **/
1877 if (logged_in) {
1878 xs *l = xs_split(cmd, "/");
1879 char *p = xs_list_get(l, -1);
1880
1881 if (p) {
1882 if (strcmp(p, "accounts") == 0) {
1883 p = xs_list_get(l, -2);
1884
1885 if (p && xs_is_hex(p)) {
1886 xs *actors = list_content(&snac1, p, NULL, 0);
1887 xs *out = xs_list_new();
1888 int c = 0;
1889 char *v;
1890
1891 while (xs_list_next(actors, &v, &c)) {
1892 xs *actor = NULL;
1893
1894 if (valid_status(object_get_by_md5(v, &actor))) {
1895 xs *acct = mastoapi_account(actor);
1896 out = xs_list_append(out, acct);
1897 }
1898 }
1899
1900 *body = xs_json_dumps(out, 4);
1901 *ctype = "application/json";
1902 status = 200;
1903 }
1904 }
1905 else
1906 if (xs_is_hex(p)) {
1907 xs *out = xs_list_new();
1908 xs *lol = list_maint(&snac1, NULL, 0);
1909 int c = 0;
1910 xs_list *v;
1911
1912 while (xs_list_next(lol, &v, &c)) {
1913 char *id = xs_list_get(v, 0);
1914
1915 if (id && strcmp(id, p) == 0) {
1916 xs *d = xs_dict_new();
1917
1918 d = xs_dict_append(d, "id", p);
1919 d = xs_dict_append(d, "title", xs_list_get(v, 1));
1920 d = xs_dict_append(d, "replies_policy", "list");
1921 d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
1922
1923 out = xs_list_append(out, d);
1924 break;
1925 }
1926 }
1927
1928 *body = xs_json_dumps(out, 4);
1929 *ctype = "application/json";
1930 status = 200;
1931 }
1932 }
1933 }
1934 }
1935 else
1936 if (strcmp(cmd, "/v1/scheduled_statuses") == 0) { /** **/
1937 /* snac does not schedule notes */
1938 *body = xs_dup("[]");
1939 *ctype = "application/json";
1940 status = 200;
1941 }
1942 else
1943 if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/
1944 /* snac does not support optional follow confirmations */
1945 *body = xs_dup("[]");
1946 *ctype = "application/json";
1947 status = 200;
1948 }
1949 else
1950 if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
1951 /* snac has no announcements (yet?) */
1952 *body = xs_dup("[]");
1953 *ctype = "application/json";
1954 status = 200;
1955 }
1956 else
1957 if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
1958 /* are you kidding me? */
1959 *body = xs_dup("[]");
1960 *ctype = "application/json";
1961 status = 200;
1962 }
1963 else
1964 if (strcmp(cmd, "/v1/instance") == 0) { /** **/
1965 /* returns an instance object */
1966 xs *ins = xs_dict_new();
1967 const char *host = xs_dict_get(srv_config, "host");
1968 const char *title = xs_dict_get(srv_config, "title");
1969 const char *sdesc = xs_dict_get(srv_config, "short_description");
1970
1971 ins = xs_dict_append(ins, "uri", host);
1972 ins = xs_dict_append(ins, "domain", host);
1973 ins = xs_dict_append(ins, "title", title && *title ? title : host);
1974 ins = xs_dict_append(ins, "version", "4.0.0 (not true; really " USER_AGENT ")");
1975 ins = xs_dict_append(ins, "source_url", WHAT_IS_SNAC_URL);
1976 ins = xs_dict_append(ins, "description", host);
1977
1978 ins = xs_dict_append(ins, "short_description", sdesc && *sdesc ? sdesc : host);
1979
1980 xs *susie = xs_fmt("%s/susie.png", srv_baseurl);
1981 ins = xs_dict_append(ins, "thumbnail", susie);
1982
1983 const char *v = xs_dict_get(srv_config, "admin_email");
1984 if (xs_is_null(v) || *v == '\0')
1985 v = "admin@localhost";
1986
1987 ins = xs_dict_append(ins, "email", v);
1988
1989 ins = xs_dict_append(ins, "rules", xs_stock(XSTYPE_LIST));
1990
1991 xs *l1 = xs_list_append(xs_list_new(), "en");
1992 ins = xs_dict_append(ins, "languages", l1);
1993
1994 xs *wss = xs_fmt("wss:/" "/%s", xs_dict_get(srv_config, "host"));
1995 xs *urls = xs_dict_new();
1996 urls = xs_dict_append(urls, "streaming_api", wss);
1997
1998 ins = xs_dict_append(ins, "urls", urls);
1999
2000 xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock(0));
2001 d2 = xs_dict_append(d2, "status_count", xs_stock(0));
2002 d2 = xs_dict_append(d2, "domain_count", xs_stock(0));
2003 ins = xs_dict_append(ins, "stats", d2);
2004
2005 ins = xs_dict_append(ins, "registrations", xs_stock(XSTYPE_FALSE));
2006 ins = xs_dict_append(ins, "approval_required", xs_stock(XSTYPE_FALSE));
2007 ins = xs_dict_append(ins, "invites_enabled", xs_stock(XSTYPE_FALSE));
2008
2009 xs *cfg = xs_dict_new();
2010
2011 {
2012 xs *d11 = xs_json_loads("{\"characters_reserved_per_url\":32,"
2013 "\"max_characters\":100000,\"max_media_attachments\":8}");
2014 cfg = xs_dict_append(cfg, "statuses", d11);
2015
2016 xs *d12 = xs_json_loads("{\"max_featured_tags\":10}");
2017 cfg = xs_dict_append(cfg, "accounts", d12);
2018
2019 xs *d13 = xs_json_loads("{\"image_matrix_limit\":33177600,"
2020 "\"image_size_limit\":16777216,"
2021 "\"video_frame_rate_limit\":120,"
2022 "\"video_matrix_limit\":8294400,"
2023 "\"video_size_limit\":103809024}"
2024 );
2025
2026 {
2027 /* get the supported mime types from the internal list */
2028 const char **p = xs_mime_types;
2029 xs_set mtypes;
2030 xs_set_init(&mtypes);
2031
2032 while (*p) {
2033 const char *type = p[1];
2034
2035 if (xs_startswith(type, "image/") ||
2036 xs_startswith(type, "video/") ||
2037 xs_startswith(type, "audio/"))
2038 xs_set_add(&mtypes, type);
2039
2040 p += 2;
2041 }
2042
2043 xs *l = xs_set_result(&mtypes);
2044 d13 = xs_dict_append(d13, "supported_mime_types", l);
2045 }
2046
2047 cfg = xs_dict_append(cfg, "media_attachments", d13);
2048
2049 xs *d14 = xs_json_loads("{\"max_characters_per_option\":50,"
2050 "\"max_expiration\":2629746,"
2051 "\"max_options\":8,\"min_expiration\":300}");
2052 cfg = xs_dict_append(cfg, "polls", d14);
2053 }
2054
2055 ins = xs_dict_append(ins, "configuration", cfg);
2056
2057 const char *admin_account = xs_dict_get(srv_config, "admin_account");
2058
2059 if (!xs_is_null(admin_account) && *admin_account) {
2060 snac admin;
2061
2062 if (user_open(&admin, admin_account)) {
2063 xs *actor = msg_actor(&admin);
2064 xs *acct = mastoapi_account(actor);
2065
2066 ins = xs_dict_append(ins, "contact_account", acct);
2067
2068 user_free(&admin);
2069 }
2070 }
2071
2072 *body = xs_json_dumps(ins, 4);
2073 *ctype = "application/json";
2074 status = 200;
2075 }
2076 else
2077 if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
2078 /* information about a status */
2079 if (logged_in) {
2080 xs *l = xs_split(cmd, "/");
2081 const char *id = xs_list_get(l, 3);
2082 const char *op = xs_list_get(l, 4);
2083
2084 if (!xs_is_null(id)) {
2085 xs *msg = NULL;
2086 xs *out = NULL;
2087
2088 /* skip the 'fake' part of the id */
2089 id = MID_TO_MD5(id);
2090
2091 if (valid_status(object_get_by_md5(id, &msg))) {
2092 if (op == NULL) {
2093 if (!is_muted(&snac1, get_atto(msg))) {
2094 /* return the status itself */
2095 out = mastoapi_status(&snac1, msg);
2096 }
2097 }
2098 else
2099 if (strcmp(op, "context") == 0) { /** **/
2100 /* return ancestors and children */
2101 xs *anc = xs_list_new();
2102 xs *des = xs_list_new();
2103 xs_list *p;
2104 xs_str *v;
2105 char pid[64];
2106
2107 /* build the [grand]parent list, moving up */
2108 strncpy(pid, id, sizeof(pid));
2109
2110 while (object_parent(pid, pid, sizeof(pid))) {
2111 xs *m2 = NULL;
2112
2113 if (valid_status(timeline_get_by_md5(&snac1, pid, &m2))) {
2114 xs *st = mastoapi_status(&snac1, m2);
2115
2116 if (st)
2117 anc = xs_list_insert(anc, 0, st);
2118 }
2119 else
2120 break;
2121 }
2122
2123 /* build the children list */
2124 xs *children = object_children(xs_dict_get(msg, "id"));
2125 p = children;
2126
2127 while (xs_list_iter(&p, &v)) {
2128 xs *m2 = NULL;
2129
2130 if (valid_status(timeline_get_by_md5(&snac1, v, &m2))) {
2131 if (xs_is_null(xs_dict_get(m2, "name"))) {
2132 xs *st = mastoapi_status(&snac1, m2);
2133
2134 if (st)
2135 des = xs_list_append(des, st);
2136 }
2137 }
2138 }
2139
2140 out = xs_dict_new();
2141 out = xs_dict_append(out, "ancestors", anc);
2142 out = xs_dict_append(out, "descendants", des);
2143 }
2144 else
2145 if (strcmp(op, "reblogged_by") == 0 || /** **/
2146 strcmp(op, "favourited_by") == 0) { /** **/
2147 /* return the list of people who liked or boosted this */
2148 out = xs_list_new();
2149
2150 xs *l = NULL;
2151
2152 if (op[0] == 'r')
2153 l = object_announces(xs_dict_get(msg, "id"));
2154 else
2155 l = object_likes(xs_dict_get(msg, "id"));
2156
2157 xs_list *p = l;
2158 xs_str *v;
2159
2160 while (xs_list_iter(&p, &v)) {
2161 xs *actor2 = NULL;
2162
2163 if (valid_status(object_get_by_md5(v, &actor2))) {
2164 xs *acct2 = mastoapi_account(actor2);
2165
2166 out = xs_list_append(out, acct2);
2167 }
2168 }
2169 }
2170 else
2171 if (strcmp(op, "source") == 0) { /** **/
2172 out = xs_dict_new();
2173
2174 /* get the mastoapi status id */
2175 out = xs_dict_append(out, "id", xs_list_get(l, 3));
2176
2177 out = xs_dict_append(out, "text", xs_dict_get(msg, "sourceContent"));
2178 out = xs_dict_append(out, "spoiler_text", xs_dict_get(msg, "summary"));
2179 }
2180 }
2181 else
2182 srv_debug(1, xs_fmt("mastoapi status: bad id %s", id));
2183
2184 if (out != NULL) {
2185 *body = xs_json_dumps(out, 4);
2186 *ctype = "application/json";
2187 status = 200;
2188 }
2189 }
2190 }
2191 else
2192 status = 401;
2193 }
2194 else
2195 if (strcmp(cmd, "/v1/preferences") == 0) { /** **/
2196 *body = xs_dup("{}");
2197 *ctype = "application/json";
2198 status = 200;
2199 }
2200 else
2201 if (strcmp(cmd, "/v1/markers") == 0) { /** **/
2202 *body = xs_dup("{}");
2203 *ctype = "application/json";
2204 status = 200;
2205 }
2206 else
2207 if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
2208 *body = xs_dup("[]");
2209 *ctype = "application/json";
2210 status = 200;
2211 }
2212 else
2213 if (strcmp(cmd, "/v2/search") == 0) { /** **/
2214 if (logged_in) {
2215 const char *q = xs_dict_get(args, "q");
2216 const char *type = xs_dict_get(args, "type");
2217 const char *offset = xs_dict_get(args, "offset");
2218
2219 xs *acl = xs_list_new();
2220 xs *stl = xs_list_new();
2221 xs *htl = xs_list_new();
2222 xs *res = xs_dict_new();
2223
2224 if (xs_is_null(offset) || strcmp(offset, "0") == 0) {
2225 /* reply something only for offset 0; otherwise,
2226 apps like Tusky keep asking again and again */
2227
2228 if (!xs_is_null(q)) {
2229 if (xs_is_null(type) || strcmp(type, "accounts") == 0) {
2230 /* do a webfinger query */
2231 char *actor = NULL;
2232 char *user = NULL;
2233
2234 if (valid_status(webfinger_request(q, &actor, &user)) && actor) {
2235 xs *actor_o = NULL;
2236
2237 if (valid_status(actor_request(&snac1, actor, &actor_o))) {
2238 xs *acct = mastoapi_account(actor_o);
2239
2240 acl = xs_list_append(acl, acct);
2241 }
2242 }
2243 }
2244
2245 if (xs_is_null(type) || strcmp(type, "hashtags") == 0) {
2246 /* search this tag */
2247 xs *tl = tag_search((char *)q, 0, 1);
2248
2249 if (xs_list_len(tl)) {
2250 xs *d = xs_dict_new();
2251
2252 d = xs_dict_append(d, "name", q);
2253 xs *url = xs_fmt("%s?t=%s", srv_baseurl, q);
2254 d = xs_dict_append(d, "url", url);
2255 d = xs_dict_append(d, "history", xs_stock(XSTYPE_LIST));
2256
2257 htl = xs_list_append(htl, d);
2258 }
2259 }
2260
2261 if (xs_is_null(type) || strcmp(type, "statuses") == 0) {
2262 int to = 0;
2263 int cnt = 40;
2264 xs *tl = content_search(&snac1, q, 1, 0, cnt, 0, &to);
2265 int c = 0;
2266 char *v;
2267
2268 while (xs_list_next(tl, &v, &c) && --cnt) {
2269 xs *post = NULL;
2270
2271 if (!valid_status(timeline_get_by_md5(&snac1, v, &post)))
2272 continue;
2273
2274 xs *s = mastoapi_status(&snac1, post);
2275
2276 if (!xs_is_null(s))
2277 stl = xs_list_append(stl, s);
2278 }
2279 }
2280 }
2281 }
2282
2283 res = xs_dict_append(res, "accounts", acl);
2284 res = xs_dict_append(res, "statuses", stl);
2285 res = xs_dict_append(res, "hashtags", htl);
2286
2287 *body = xs_json_dumps(res, 4);
2288 *ctype = "application/json";
2289 status = 200;
2290 }
2291 else
2292 status = 401;
2293 }
2294
2295 /* user cleanup */
2296 if (logged_in)
2297 user_free(&snac1);
2298
2299 srv_debug(1, xs_fmt("mastoapi_get_handler %s %d", q_path, status));
2300
2301 return status;
2302}
2303
2304
2305int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2306 const char *payload, int p_size,
2307 char **body, int *b_size, char **ctype)
2308{
2309 (void)p_size;
2310 (void)b_size;
2311
2312 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2313 return 0;
2314
2315 int status = 404;
2316 xs *args = NULL;
2317 char *i_ctype = xs_dict_get(req, "content-type");
2318
2319 if (i_ctype && xs_startswith(i_ctype, "application/json")) {
2320 if (!xs_is_null(payload))
2321 args = xs_json_loads(payload);
2322 }
2323 else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
2324 {
2325 // Some apps send form data instead of json so we should cater for those
2326 if (!xs_is_null(payload)) {
2327 xs *upl = xs_url_dec(payload);
2328 args = xs_url_vars(upl);
2329 }
2330 }
2331 else
2332 args = xs_dup(xs_dict_get(req, "p_vars"));
2333
2334 if (args == NULL)
2335 return 400;
2336
2337 xs *cmd = xs_replace_n(q_path, "/api", "", 1);
2338
2339 snac snac = {0};
2340 int logged_in = process_auth_token(&snac, req);
2341
2342 if (strcmp(cmd, "/v1/apps") == 0) { /** **/
2343 const char *name = xs_dict_get(args, "client_name");
2344 const char *ruri = xs_dict_get(args, "redirect_uris");
2345 const char *scope = xs_dict_get(args, "scope");
2346
2347 if (xs_type(ruri) == XSTYPE_LIST)
2348 ruri = xs_dict_get(ruri, 0);
2349
2350 if (name && ruri) {
2351 xs *app = xs_dict_new();
2352 xs *id = xs_replace_i(tid(0), ".", "");
2353 xs *csec = random_str();
2354 xs *vkey = random_str();
2355 xs *cid = NULL;
2356
2357 /* pick a non-existent random cid */
2358 for (;;) {
2359 cid = random_str();
2360 xs *p_app = app_get(cid);
2361
2362 if (p_app == NULL)
2363 break;
2364
2365 xs_free(cid);
2366 }
2367
2368 app = xs_dict_append(app, "name", name);
2369 app = xs_dict_append(app, "redirect_uri", ruri);
2370 app = xs_dict_append(app, "client_id", cid);
2371 app = xs_dict_append(app, "client_secret", csec);
2372 app = xs_dict_append(app, "vapid_key", vkey);
2373 app = xs_dict_append(app, "id", id);
2374
2375 *body = xs_json_dumps(app, 4);
2376 *ctype = "application/json";
2377 status = 200;
2378
2379 app = xs_dict_append(app, "code", "");
2380
2381 if (scope)
2382 app = xs_dict_append(app, "scope", scope);
2383
2384 app_add(cid, app);
2385
2386 srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
2387 }
2388 }
2389 else
2390 if (strcmp(cmd, "/v1/statuses") == 0) { /** **/
2391 if (logged_in) {
2392 /* post a new Note */
2393 const char *content = xs_dict_get(args, "status");
2394 const char *mid = xs_dict_get(args, "in_reply_to_id");
2395 const char *visibility = xs_dict_get(args, "visibility");
2396 const char *summary = xs_dict_get(args, "spoiler_text");
2397 const char *media_ids = xs_dict_get(args, "media_ids");
2398
2399 if (xs_is_null(media_ids))
2400 media_ids = xs_dict_get(args, "media_ids[]");
2401
2402 if (xs_is_null(media_ids))
2403 media_ids = xs_dict_get(args, "media_ids");
2404
2405 if (xs_is_null(visibility))
2406 visibility = "public";
2407
2408 xs *attach_list = xs_list_new();
2409 xs *irt = NULL;
2410
2411 /* is it a reply? */
2412 if (mid != NULL) {
2413 xs *r_msg = NULL;
2414 const char *md5 = MID_TO_MD5(mid);
2415
2416 if (valid_status(object_get_by_md5(md5, &r_msg)))
2417 irt = xs_dup(xs_dict_get(r_msg, "id"));
2418 }
2419
2420 /* does it have attachments? */
2421 if (!xs_is_null(media_ids)) {
2422 xs *mi = NULL;
2423
2424 if (xs_type(media_ids) == XSTYPE_LIST)
2425 mi = xs_dup(media_ids);
2426 else {
2427 mi = xs_list_new();
2428 mi = xs_list_append(mi, media_ids);
2429 }
2430
2431 xs_list *p = mi;
2432 xs_str *v;
2433
2434 while (xs_list_iter(&p, &v)) {
2435 xs *l = xs_list_new();
2436 xs *url = xs_fmt("%s/s/%s", snac.actor, v);
2437 xs *desc = static_get_meta(&snac, v);
2438
2439 l = xs_list_append(l, url);
2440 l = xs_list_append(l, desc);
2441
2442 attach_list = xs_list_append(attach_list, l);
2443 }
2444 }
2445
2446 /* prepare the message */
2447 xs *msg = msg_note(&snac, content, NULL, irt, attach_list,
2448 strcmp(visibility, "public") == 0 ? 0 : 1);
2449
2450 if (!xs_is_null(summary) && *summary) {
2451 msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
2452 msg = xs_dict_set(msg, "summary", summary);
2453 }
2454
2455 /* store */
2456 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
2457
2458 /* 'Create' message */
2459 xs *c_msg = msg_create(&snac, msg);
2460 enqueue_message(&snac, c_msg);
2461
2462 timeline_touch(&snac);
2463
2464 /* convert to a mastodon status as a response code */
2465 xs *st = mastoapi_status(&snac, msg);
2466
2467 *body = xs_json_dumps(st, 4);
2468 *ctype = "application/json";
2469 status = 200;
2470 }
2471 else
2472 status = 401;
2473 }
2474 else
2475 if (xs_startswith(cmd, "/v1/statuses")) { /** **/
2476 if (logged_in) {
2477 /* operations on a status */
2478 xs *l = xs_split(cmd, "/");
2479 const char *mid = xs_list_get(l, 3);
2480 const char *op = xs_list_get(l, 4);
2481
2482 if (!xs_is_null(mid)) {
2483 xs *msg = NULL;
2484 xs *out = NULL;
2485
2486 /* skip the 'fake' part of the id */
2487 mid = MID_TO_MD5(mid);
2488
2489 if (valid_status(timeline_get_by_md5(&snac, mid, &msg))) {
2490 char *id = xs_dict_get(msg, "id");
2491
2492 if (op == NULL) {
2493 /* no operation (?) */
2494 }
2495 else
2496 if (strcmp(op, "favourite") == 0) { /** **/
2497 xs *n_msg = msg_admiration(&snac, id, "Like");
2498
2499 if (n_msg != NULL) {
2500 enqueue_message(&snac, n_msg);
2501 timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1);
2502
2503 out = mastoapi_status(&snac, msg);
2504 }
2505 }
2506 else
2507 if (strcmp(op, "unfavourite") == 0) { /** **/
2508 xs *n_msg = msg_repulsion(&snac, id, "Like");
2509
2510 if (n_msg != NULL) {
2511 enqueue_message(&snac, n_msg);
2512
2513 out = mastoapi_status(&snac, msg);
2514 }
2515 }
2516 else
2517 if (strcmp(op, "reblog") == 0) { /** **/
2518 xs *n_msg = msg_admiration(&snac, id, "Announce");
2519
2520 if (n_msg != NULL) {
2521 enqueue_message(&snac, n_msg);
2522 timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0);
2523
2524 out = mastoapi_status(&snac, msg);
2525 }
2526 }
2527 else
2528 if (strcmp(op, "unreblog") == 0) { /** **/
2529 xs *n_msg = msg_repulsion(&snac, id, "Announce");
2530
2531 if (n_msg != NULL) {
2532 enqueue_message(&snac, n_msg);
2533
2534 out = mastoapi_status(&snac, msg);
2535 }
2536 }
2537 else
2538 if (strcmp(op, "bookmark") == 0) { /** **/
2539 /* snac does not support bookmarks */
2540 }
2541 else
2542 if (strcmp(op, "unbookmark") == 0) { /** **/
2543 /* snac does not support bookmarks */
2544 }
2545 else
2546 if (strcmp(op, "pin") == 0) { /** **/
2547 /* pin this message */
2548 if (pin(&snac, id))
2549 out = mastoapi_status(&snac, msg);
2550 else
2551 status = 422;
2552 }
2553 else
2554 if (strcmp(op, "unpin") == 0) { /** **/
2555 /* unpin this message */
2556 unpin(&snac, id);
2557 out = mastoapi_status(&snac, msg);
2558 }
2559 else
2560 if (strcmp(op, "mute") == 0) { /** **/
2561 /* Mastodon's mute is snac's hide */
2562 }
2563 else
2564 if (strcmp(op, "unmute") == 0) { /** **/
2565 /* Mastodon's unmute is snac's unhide */
2566 }
2567 }
2568
2569 if (out != NULL) {
2570 *body = xs_json_dumps(out, 4);
2571 *ctype = "application/json";
2572 status = 200;
2573 }
2574 }
2575 }
2576 else
2577 status = 401;
2578 }
2579 else
2580 if (strcmp(cmd, "/v1/notifications/clear") == 0) { /** **/
2581 if (logged_in) {
2582 notify_clear(&snac);
2583 timeline_touch(&snac);
2584
2585 *body = xs_dup("{}");
2586 *ctype = "application/json";
2587 status = 200;
2588 }
2589 else
2590 status = 401;
2591 }
2592 else
2593 if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
2594 /* I don't know what I'm doing */
2595 if (logged_in) {
2596 char *v;
2597
2598 xs *wpush = xs_dict_new();
2599
2600 wpush = xs_dict_append(wpush, "id", "1");
2601
2602 v = xs_dict_get(args, "data");
2603 v = xs_dict_get(v, "alerts");
2604 wpush = xs_dict_append(wpush, "alerts", v);
2605
2606 v = xs_dict_get(args, "subscription");
2607 v = xs_dict_get(v, "endpoint");
2608 wpush = xs_dict_append(wpush, "endpoint", v);
2609
2610 xs *server_key = random_str();
2611 wpush = xs_dict_append(wpush, "server_key", server_key);
2612
2613 *body = xs_json_dumps(wpush, 4);
2614 *ctype = "application/json";
2615 status = 200;
2616 }
2617 else
2618 status = 401;
2619 }
2620 else
2621 if (strcmp(cmd, "/v1/media") == 0 || strcmp(cmd, "/v2/media") == 0) { /** **/
2622 if (logged_in) {
2623 const xs_list *file = xs_dict_get(args, "file");
2624 const char *desc = xs_dict_get(args, "description");
2625
2626 if (xs_is_null(desc))
2627 desc = "";
2628
2629 status = 400;
2630
2631 if (xs_type(file) == XSTYPE_LIST) {
2632 const char *fn = xs_list_get(file, 0);
2633
2634 if (*fn != '\0') {
2635 char *ext = strrchr(fn, '.');
2636 xs *hash = xs_md5_hex(fn, strlen(fn));
2637 xs *id = xs_fmt("%s%s", hash, ext);
2638 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
2639 int fo = xs_number_get(xs_list_get(file, 1));
2640 int fs = xs_number_get(xs_list_get(file, 2));
2641
2642 /* store */
2643 static_put(&snac, id, payload + fo, fs);
2644 static_put_meta(&snac, id, desc);
2645
2646 /* prepare a response */
2647 xs *rsp = xs_dict_new();
2648
2649 rsp = xs_dict_append(rsp, "id", id);
2650 rsp = xs_dict_append(rsp, "type", "image");
2651 rsp = xs_dict_append(rsp, "url", url);
2652 rsp = xs_dict_append(rsp, "preview_url", url);
2653 rsp = xs_dict_append(rsp, "remote_url", url);
2654 rsp = xs_dict_append(rsp, "description", desc);
2655
2656 *body = xs_json_dumps(rsp, 4);
2657 *ctype = "application/json";
2658 status = 200;
2659 }
2660 }
2661 }
2662 else
2663 status = 401;
2664 }
2665 else
2666 if (xs_startswith(cmd, "/v1/accounts")) { /** **/
2667 if (logged_in) {
2668 /* account-related information */
2669 xs *l = xs_split(cmd, "/");
2670 const char *md5 = xs_list_get(l, 3);
2671 const char *opt = xs_list_get(l, 4);
2672 xs *rsp = NULL;
2673
2674 if (!xs_is_null(md5) && *md5) {
2675 xs *actor_o = NULL;
2676
2677 if (xs_is_null(opt)) {
2678 /* ? */
2679 }
2680 else
2681 if (strcmp(opt, "follow") == 0) { /** **/
2682 if (valid_status(object_get_by_md5(md5, &actor_o))) {
2683 const char *actor = xs_dict_get(actor_o, "id");
2684
2685 xs *msg = msg_follow(&snac, actor);
2686
2687 if (msg != NULL) {
2688 /* reload the actor from the message, in may be different */
2689 actor = xs_dict_get(msg, "object");
2690
2691 following_add(&snac, actor, msg);
2692
2693 enqueue_output_by_actor(&snac, msg, actor, 0);
2694
2695 rsp = mastoapi_relationship(&snac, md5);
2696 }
2697 }
2698 }
2699 else
2700 if (strcmp(opt, "unfollow") == 0) { /** **/
2701 if (valid_status(object_get_by_md5(md5, &actor_o))) {
2702 const char *actor = xs_dict_get(actor_o, "id");
2703
2704 /* get the following object */
2705 xs *object = NULL;
2706
2707 if (valid_status(following_get(&snac, actor, &object))) {
2708 xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
2709
2710 following_del(&snac, actor);
2711
2712 enqueue_output_by_actor(&snac, msg, actor, 0);
2713
2714 rsp = mastoapi_relationship(&snac, md5);
2715 }
2716 }
2717 }
2718 else
2719 if (strcmp(opt, "block") == 0) { /** **/
2720 if (valid_status(object_get_by_md5(md5, &actor_o))) {
2721 const char *actor = xs_dict_get(actor_o, "id");
2722
2723 mute(&snac, actor);
2724
2725 rsp = mastoapi_relationship(&snac, md5);
2726 }
2727 }
2728 else
2729 if (strcmp(opt, "unblock") == 0) { /** **/
2730 if (valid_status(object_get_by_md5(md5, &actor_o))) {
2731 const char *actor = xs_dict_get(actor_o, "id");
2732
2733 unmute(&snac, actor);
2734
2735 rsp = mastoapi_relationship(&snac, md5);
2736 }
2737 }
2738 }
2739
2740 if (rsp != NULL) {
2741 *body = xs_json_dumps(rsp, 4);
2742 *ctype = "application/json";
2743 status = 200;
2744 }
2745 }
2746 else
2747 status = 401;
2748 }
2749 else
2750 if (xs_startswith(cmd, "/v1/polls")) { /** **/
2751 if (logged_in) {
2752 /* operations on a status */
2753 xs *l = xs_split(cmd, "/");
2754 const char *mid = xs_list_get(l, 3);
2755 const char *op = xs_list_get(l, 4);
2756
2757 if (!xs_is_null(mid)) {
2758 xs *msg = NULL;
2759 xs *out = NULL;
2760
2761 /* skip the 'fake' part of the id */
2762 mid = MID_TO_MD5(mid);
2763
2764 if (valid_status(timeline_get_by_md5(&snac, mid, &msg))) {
2765 const char *id = xs_dict_get(msg, "id");
2766 const char *atto = get_atto(msg);
2767
2768 xs_list *opts = xs_dict_get(msg, "oneOf");
2769 if (opts == NULL)
2770 opts = xs_dict_get(msg, "anyOf");
2771
2772 if (op == NULL) {
2773 }
2774 else
2775 if (strcmp(op, "votes") == 0) {
2776 xs_list *choices = xs_dict_get(args, "choices[]");
2777
2778 if (xs_is_null(choices))
2779 choices = xs_dict_get(args, "choices");
2780
2781 if (xs_type(choices) == XSTYPE_LIST) {
2782 xs_str *v;
2783
2784 while (xs_list_iter(&choices, &v)) {
2785 int io = atoi(v);
2786 const xs_dict *o = xs_list_get(opts, io);
2787
2788 if (o) {
2789 const char *name = xs_dict_get(o, "name");
2790
2791 xs *msg = msg_note(&snac, "", atto, (char *)id, NULL, 1);
2792 msg = xs_dict_append(msg, "name", name);
2793
2794 xs *c_msg = msg_create(&snac, msg);
2795 enqueue_message(&snac, c_msg);
2796 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
2797 }
2798 }
2799
2800 out = mastoapi_poll(&snac, msg);
2801 }
2802 }
2803 }
2804
2805 if (out != NULL) {
2806 *body = xs_json_dumps(out, 4);
2807 *ctype = "application/json";
2808 status = 200;
2809 }
2810 }
2811 }
2812 else
2813 status = 401;
2814 }
2815 else
2816 if (strcmp(cmd, "/v1/lists") == 0) {
2817 if (logged_in) {
2818 const char *title = xs_dict_get(args, "title");
2819
2820 if (xs_type(title) == XSTYPE_STRING) {
2821 /* add the list */
2822 xs *out = xs_dict_new();
2823
2824 if (xs_type(list_maint(&snac, title, 1)) == XSTYPE_TRUE) {
2825 out = xs_dict_append(out, "title", title);
2826 out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
2827 out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
2828
2829 status = 200;
2830 }
2831 else {
2832 out = xs_dict_append(out, "error", "cannot create list");
2833 status = 422;
2834 }
2835
2836 *body = xs_json_dumps(out, 4);
2837 *ctype = "application/json";
2838 }
2839 else
2840 status = 422;
2841 }
2842 }
2843 if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
2844 if (logged_in) {
2845 xs *l = xs_split(cmd, "/");
2846 char *op = xs_list_get(l, -1);
2847 char *id = xs_list_get(l, -2);
2848
2849 if (op && id && xs_is_hex(id)) {
2850 if (strcmp(op, "accounts") == 0) {
2851 xs_list *accts = xs_dict_get(args, "account_ids[]");
2852 int c = 0;
2853 char *v;
2854
2855 while (xs_list_next(accts, &v, &c)) {
2856 list_content(&snac, id, v, 1);
2857 }
2858
2859 status = 200;
2860 }
2861 }
2862 }
2863 else
2864 status = 422;
2865 }
2866
2867 /* user cleanup */
2868 if (logged_in)
2869 user_free(&snac);
2870
2871 srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status));
2872
2873 return status;
2874}
2875
2876
2877int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
2878 const char *payload, int p_size,
2879 char **body, int *b_size, char **ctype)
2880{
2881 (void)p_size;
2882 (void)body;
2883 (void)b_size;
2884 (void)ctype;
2885
2886 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2887 return 0;
2888
2889 int status = 404;
2890 xs *args = NULL;
2891 char *i_ctype = xs_dict_get(req, "content-type");
2892
2893 if (i_ctype && xs_startswith(i_ctype, "application/json")) {
2894 if (!xs_is_null(payload))
2895 args = xs_json_loads(payload);
2896 }
2897 else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
2898 {
2899 // Some apps send form data instead of json so we should cater for those
2900 if (!xs_is_null(payload)) {
2901 xs *upl = xs_url_dec(payload);
2902 args = xs_url_vars(upl);
2903 }
2904 }
2905 else
2906 args = xs_dup(xs_dict_get(req, "p_vars"));
2907
2908 if (args == NULL)
2909 return 400;
2910
2911 snac snac = {0};
2912 int logged_in = process_auth_token(&snac, req);
2913
2914 xs *cmd = xs_replace_n(q_path, "/api", "", 1);
2915
2916 if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
2917 // pretend we deleted it, since it doesn't exist anyway
2918 status = 200;
2919 }
2920 else
2921 if (xs_startswith(cmd, "/v1/lists/")) {
2922 if (logged_in) {
2923 xs *l = xs_split(cmd, "/");
2924 char *p = xs_list_get(l, -1);
2925
2926 if (p) {
2927 if (strcmp(p, "accounts") == 0) {
2928 /* delete account from list */
2929 p = xs_list_get(l, -2);
2930 xs_list *accts = xs_dict_get(args, "account_ids[]");
2931 int c = 0;
2932 char *v;
2933
2934 while (xs_list_next(accts, &v, &c)) {
2935 list_content(&snac, p, v, 2);
2936 }
2937 }
2938 else {
2939 /* delete list */
2940 if (xs_is_hex(p)) {
2941 list_maint(&snac, p, 2);
2942 }
2943 }
2944 }
2945
2946 status = 200;
2947 }
2948 else
2949 status = 401;
2950 }
2951
2952 /* user cleanup */
2953 if (logged_in)
2954 user_free(&snac);
2955
2956 srv_debug(1, xs_fmt("mastoapi_delete_handler %s %d", q_path, status));
2957
2958 return status;
2959}
2960
2961
2962int mastoapi_put_handler(const xs_dict *req, const char *q_path,
2963 const char *payload, int p_size,
2964 char **body, int *b_size, char **ctype)
2965{
2966 (void)p_size;
2967 (void)b_size;
2968
2969 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2970 return 0;
2971
2972 int status = 404;
2973 xs *args = NULL;
2974 char *i_ctype = xs_dict_get(req, "content-type");
2975
2976 if (i_ctype && xs_startswith(i_ctype, "application/json")) {
2977 if (!xs_is_null(payload))
2978 args = xs_json_loads(payload);
2979 }
2980 else
2981 args = xs_dup(xs_dict_get(req, "p_vars"));
2982
2983 if (args == NULL)
2984 return 400;
2985
2986 xs *cmd = xs_replace_n(q_path, "/api", "", 1);
2987
2988 snac snac = {0};
2989 int logged_in = process_auth_token(&snac, req);
2990
2991 if (xs_startswith(cmd, "/v1/media") || xs_startswith(cmd, "/v2/media")) { /** **/
2992 if (logged_in) {
2993 xs *l = xs_split(cmd, "/");
2994 const char *stid = xs_list_get(l, 3);
2995
2996 if (!xs_is_null(stid)) {
2997 const char *desc = xs_dict_get(args, "description");
2998
2999 /* set the image metadata */
3000 static_put_meta(&snac, stid, desc);
3001
3002 /* prepare a response */
3003 xs *rsp = xs_dict_new();
3004 xs *url = xs_fmt("%s/s/%s", snac.actor, stid);
3005
3006 rsp = xs_dict_append(rsp, "id", stid);
3007 rsp = xs_dict_append(rsp, "type", "image");
3008 rsp = xs_dict_append(rsp, "url", url);
3009 rsp = xs_dict_append(rsp, "preview_url", url);
3010 rsp = xs_dict_append(rsp, "remote_url", url);
3011 rsp = xs_dict_append(rsp, "description", desc);
3012
3013 *body = xs_json_dumps(rsp, 4);
3014 *ctype = "application/json";
3015 status = 200;
3016 }
3017 }
3018 else
3019 status = 401;
3020 }
3021 else
3022 if (xs_startswith(cmd, "/v1/statuses")) {
3023 if (logged_in) {
3024 xs *l = xs_split(cmd, "/");
3025 const char *mid = xs_list_get(l, 3);
3026
3027 if (!xs_is_null(mid)) {
3028 const char *md5 = MID_TO_MD5(mid);
3029 xs *rsp = NULL;
3030 xs *msg = NULL;
3031
3032 if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) {
3033 const char *content = xs_dict_get(args, "status");
3034 xs *atls = xs_list_new();
3035 xs *f_content = not_really_markdown(content, &atls, NULL);
3036
3037 /* replace fields with new content */
3038 msg = xs_dict_set(msg, "sourceContent", content);
3039 msg = xs_dict_set(msg, "content", f_content);
3040
3041 xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
3042 msg = xs_dict_set(msg, "updated", updated);
3043
3044 /* overwrite object, not updating the indexes */
3045 object_add_ow(xs_dict_get(msg, "id"), msg);
3046
3047 /* update message */
3048 xs *c_msg = msg_update(&snac, msg);
3049
3050 enqueue_message(&snac, c_msg);
3051
3052 rsp = mastoapi_status(&snac, msg);
3053 }
3054
3055 if (rsp != NULL) {
3056 *body = xs_json_dumps(rsp, 4);
3057 *ctype = "application/json";
3058 status = 200;
3059 }
3060 }
3061 }
3062 else
3063 status = 401;
3064 }
3065
3066 /* user cleanup */
3067 if (logged_in)
3068 user_free(&snac);
3069
3070 srv_debug(1, xs_fmt("mastoapi_put_handler %s %d", q_path, status));
3071
3072 return status;
3073}
3074
3075
3076void mastoapi_purge(void)
3077{
3078 xs *spec = xs_fmt("%s/app/" "*.json", srv_basedir);
3079 xs *files = xs_glob(spec, 1, 0);
3080 xs_list *p = files;
3081 xs_str *v;
3082
3083 time_t mt = time(NULL) - 3600;
3084
3085 while (xs_list_iter(&p, &v)) {
3086 xs *cid = xs_replace(v, ".json", "");
3087 xs *fn = _app_fn(cid);
3088
3089 if (mtime(fn) < mt) {
3090 /* get the app */
3091 xs *app = app_get(cid);
3092
3093 if (app) {
3094 /* old apps with no uid are incomplete cruft */
3095 const char *uid = xs_dict_get(app, "uid");
3096
3097 if (xs_is_null(uid) || *uid == '\0') {
3098 unlink(fn);
3099 srv_debug(2, xs_fmt("purged %s", fn));
3100 }
3101 }
3102 }
3103 }
3104}
3105
3106
3107#endif /* #ifndef NO_MASTODON_API */