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