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