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