2.93
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 = atoi(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_fmt("%d", 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, 0);
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, 0);
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 /* TBD */
2311 *body = xs_dup("[]");
2312 *ctype = "application/json";
2313 status = HTTP_STATUS_OK;
2314 }
2315 else
2316 if (strcmp(cmd, "/v1/notifications") == 0) { /** **/
2317 if (logged_in) {
2318 xs *l = notify_list(&snac1, 0, 64);
2319 xs *out = xs_list_new();
2320 const char *v;
2321 const xs_list *excl = xs_dict_get(args, "exclude_types[]");
2322 const xs_list *incl = xs_dict_get(args, "types[]");
2323 const char *min_id = xs_dict_get(args, "min_id");
2324 const char *max_id = xs_dict_get(args, "max_id");
2325 const char *limit = xs_dict_get(args, "limit");
2326 int limit_count = 0;
2327 if (xs_is_string(limit)) {
2328 limit_count = atoi(limit);
2329 }
2330
2331 if (dbglevel) {
2332 xs *js = xs_json_dumps(args, 0);
2333 srv_debug(1, xs_fmt("mastoapi_notifications args %s", js));
2334 }
2335
2336 xs_list_foreach(l, v) {
2337 xs *noti = notify_get(&snac1, v);
2338
2339 if (noti == NULL)
2340 continue;
2341
2342 const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0);
2343
2344 const char *type = xs_dict_get(noti, "type");
2345 const char *utype = xs_dict_get(noti, "utype");
2346 const char *objid = xs_dict_get(noti, "objid");
2347 const char *id = xs_dict_get(noti, "id");
2348 const char *actid = xs_dict_get(noti, "actor");
2349
2350 int isEmoji = 0;
2351
2352 xs *fid = xs_replace(id, ".", "");
2353 xs *actor = NULL;
2354 xs *entry = NULL;
2355
2356 if (tag) {
2357 isEmoji = strcmp(xs_dict_get(tag, "type"), "Emoji") ? 0 : 1;
2358 }
2359
2360 if (!valid_status(actor_get(actid, &actor)))
2361 continue;
2362
2363 if (objid != NULL && !valid_status(object_get(objid, &entry)))
2364 continue;
2365
2366 if (is_hidden(&snac1, objid))
2367 continue;
2368
2369 if (max_id) {
2370 if (strcmp(fid, max_id) == 0)
2371 max_id = NULL;
2372
2373 continue;
2374 }
2375
2376 if (min_id) {
2377 if (strcmp(fid, min_id) <= 0) {
2378 continue;
2379 }
2380 }
2381
2382 /* convert the type */
2383 if (strcmp(type, "Like") == 0 && !isEmoji)
2384 type = "favourite";
2385 else
2386 if (isEmoji || strcmp(type, "EmojiReact") == 0)
2387 type = "reaction";
2388 else
2389 if (strcmp(type, "Announce") == 0)
2390 type = "reblog";
2391 else
2392 if (strcmp(type, "Follow") == 0)
2393 type = "follow";
2394 else
2395 if (strcmp(type, "Create") == 0)
2396 type = "mention";
2397 else
2398 if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
2399 type = "poll";
2400 else
2401 continue;
2402
2403 /* excluded type? */
2404 if (xs_is_list(excl) && xs_list_in(excl, type) != -1)
2405 continue;
2406
2407 /* included type? */
2408 if (xs_is_list(incl) && xs_list_in(incl, type) == -1)
2409 continue;
2410
2411 xs *mn = xs_dict_new();
2412
2413 mn = xs_dict_append(mn, "type", type);
2414
2415 mn = xs_dict_append(mn, "id", fid);
2416
2417 mn = xs_dict_append(mn, "created_at", xs_dict_get(noti, "date"));
2418
2419 xs *acct = mastoapi_account(&snac1, actor);
2420
2421 if (acct == NULL)
2422 continue;
2423
2424 mn = xs_dict_append(mn, "account", acct);
2425
2426 if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) {
2427 xs *st = mastoapi_status(&snac1, entry);
2428
2429 if (st) {
2430 mn = xs_dict_append(mn, "status", st);
2431
2432 if (strcmp(type, "reaction") == 0 && !xs_is_null(objid)) {
2433 const char *eid = NULL;
2434 const char *url = NULL;
2435 int utf = 0;
2436
2437 const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0);
2438 const char *content = xs_dict_get_path(noti, "msg.content");
2439
2440 url = xs_dict_get(xs_dict_get(tag, "icon"), "url");
2441 eid = xs_dict_get(tag, "name");
2442
2443 if (eid && url) {
2444 mn = xs_dict_append(mn, "emoji", eid);
2445 mn = xs_dict_append(mn, "emoji_url", url);
2446 }
2447
2448 if (xs_is_emoji((utf = xs_utf8_dec(&content)))) {
2449 xs *s1 = xs_fmt("&#%d;", utf);
2450 mn = xs_dict_append(mn, "name", s1);
2451 }
2452 }
2453 }
2454 }
2455
2456 out = xs_list_append(out, mn);
2457
2458 if (--limit_count <= 0)
2459 break;
2460 }
2461
2462 srv_debug(1, xs_fmt("mastoapi_notifications count %d", xs_list_len(out)));
2463
2464 *body = xs_json_dumps(out, 4);
2465 *ctype = "application/json";
2466 status = HTTP_STATUS_OK;
2467 }
2468 else
2469 status = HTTP_STATUS_UNAUTHORIZED;
2470 }
2471 else
2472 if (strcmp(cmd, "/v1/filters") == 0) { /** **/
2473 /* snac will never have filters */
2474 *body = xs_dup("[]");
2475 *ctype = "application/json";
2476 status = HTTP_STATUS_OK;
2477 }
2478 else
2479 if (strcmp(cmd, "/v2/filters") == 0) { /** **/
2480 /* snac will never have filters
2481 * but still, without a v2 endpoint a short delay is introduced
2482 * in some apps */
2483 *body = xs_dup("[]");
2484 *ctype = "application/json";
2485 status = HTTP_STATUS_OK;
2486 }
2487 else
2488 if (strcmp(cmd, "/v1/favourites") == 0) { /** **/
2489 /* snac will never support a list of favourites */
2490 *body = xs_dup("[]");
2491 *ctype = "application/json";
2492 status = HTTP_STATUS_OK;
2493 }
2494 else
2495 if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/
2496 if (logged_in) {
2497 xs *ifn = bookmark_index_fn(&snac1);
2498 xs *out = mastoapi_timeline(&snac1, args, ifn);
2499
2500 *body = xs_json_dumps(out, 4);
2501 *ctype = "application/json";
2502 status = HTTP_STATUS_OK;
2503 }
2504 else
2505 status = HTTP_STATUS_UNAUTHORIZED;
2506 }
2507 else
2508 if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
2509 if (logged_in) {
2510 xs *l = mastoapi_account_lists(&snac1, NULL);
2511
2512 *body = xs_json_dumps(l, 4);
2513 *ctype = "application/json";
2514 status = HTTP_STATUS_OK;
2515 }
2516 else
2517 status = HTTP_STATUS_UNAUTHORIZED;
2518 }
2519 else
2520 if (xs_startswith(cmd, "/v1/lists/")) { /** list information **/
2521 if (logged_in) {
2522 xs *l = xs_split(cmd, "/");
2523 const char *p = xs_list_get(l, -1);
2524
2525 if (p) {
2526 if (strcmp(p, "accounts") == 0) {
2527 p = xs_list_get(l, -2);
2528
2529 if (p && xs_is_hex(p)) {
2530 xs *actors = list_members(&snac1, p, NULL, 0);
2531 xs *out = xs_list_new();
2532 int c = 0;
2533 const char *v;
2534
2535 while (xs_list_next(actors, &v, &c)) {
2536 xs *actor = NULL;
2537
2538 if (valid_status(object_get_by_md5(v, &actor))) {
2539 xs *acct = mastoapi_account(&snac1, actor);
2540 out = xs_list_append(out, acct);
2541 }
2542 }
2543
2544 *body = xs_json_dumps(out, 4);
2545 *ctype = "application/json";
2546 status = HTTP_STATUS_OK;
2547 }
2548 }
2549 else
2550 if (xs_is_hex(p)) {
2551 xs *out = xs_list_new();
2552 xs *lol = list_maint(&snac1, NULL, 0);
2553 int c = 0;
2554 const xs_list *v;
2555
2556 while (xs_list_next(lol, &v, &c)) {
2557 const char *id = xs_list_get(v, 0);
2558
2559 if (id && strcmp(id, p) == 0) {
2560 xs *d = xs_dict_new();
2561
2562 d = xs_dict_append(d, "id", p);
2563 d = xs_dict_append(d, "title", xs_list_get(v, 1));
2564 d = xs_dict_append(d, "replies_policy", "list");
2565 d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
2566
2567 out = xs_dup(d);
2568 break;
2569 }
2570 }
2571
2572 *body = xs_json_dumps(out, 4);
2573 *ctype = "application/json";
2574 status = HTTP_STATUS_OK;
2575 }
2576 }
2577 }
2578 else
2579 status = HTTP_STATUS_UNAUTHORIZED;
2580 }
2581 else
2582 if (strcmp(cmd, "/v1/scheduled_statuses") == 0) { /** **/
2583 /* TBD */
2584 *body = xs_dup("[]");
2585 *ctype = "application/json";
2586 status = HTTP_STATUS_OK;
2587 }
2588 else
2589 if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/
2590 if (logged_in) {
2591 xs *pend = pending_list(&snac1);
2592 xs *resp = xs_list_new();
2593 const char *id;
2594
2595 xs_list_foreach(pend, id) {
2596 xs *actor = NULL;
2597
2598 if (valid_status(object_get(id, &actor))) {
2599 xs *acct = mastoapi_account(&snac1, actor);
2600
2601 if (acct)
2602 resp = xs_list_append(resp, acct);
2603 }
2604 }
2605
2606 *body = xs_json_dumps(resp, 4);
2607 *ctype = "application/json";
2608 status = HTTP_STATUS_OK;
2609 }
2610 }
2611 else
2612 if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
2613 if (logged_in) {
2614 xs *resp = xs_list_new();
2615 double la = 0.0;
2616 xs *user_la = xs_dup(xs_dict_get(snac1.config, "last_announcement"));
2617 if (user_la != NULL)
2618 la = xs_number_get(user_la);
2619 xs *val_date = xs_str_utctime(la, ISO_DATE_SPEC);
2620
2621 /* contrary to html, we always send the announcement and set the read flag instead */
2622
2623 const t_announcement *annce = announcement(la);
2624 if (annce != NULL && annce->text != NULL) {
2625 xs *an = xs_dict_new();
2626 xs *id = xs_fmt("%d", annce->timestamp);
2627 xs *ct = xs_fmt("<p>%s</p>", annce->text);
2628
2629 an = xs_dict_set(an, "id", id);
2630 an = xs_dict_set(an, "content", ct);
2631 an = xs_dict_set(an, "starts_at", xs_stock(XSTYPE_NULL));
2632 an = xs_dict_set(an, "ends_at", xs_stock(XSTYPE_NULL));
2633 an = xs_dict_set(an, "all_day", xs_stock(XSTYPE_TRUE));
2634 an = xs_dict_set(an, "published_at", val_date);
2635 an = xs_dict_set(an, "updated_at", val_date);
2636 an = xs_dict_set(an, "read", (annce->timestamp >= la)
2637 ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE));
2638 an = xs_dict_set(an, "mentions", xs_stock(XSTYPE_LIST));
2639 an = xs_dict_set(an, "statuses", xs_stock(XSTYPE_LIST));
2640 an = xs_dict_set(an, "tags", xs_stock(XSTYPE_LIST));
2641 an = xs_dict_set(an, "emojis", xs_stock(XSTYPE_LIST));
2642 an = xs_dict_set(an, "reactions", xs_stock(XSTYPE_LIST));
2643 resp = xs_list_append(resp, an);
2644 }
2645
2646 *body = xs_json_dumps(resp, 4);
2647 *ctype = "application/json";
2648 status = HTTP_STATUS_OK;
2649 }
2650 }
2651 else
2652 if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
2653 xs *emo = emojis();
2654 xs *list = xs_list_new();
2655 const xs_str *k;
2656 const xs_val *v;
2657 xs_dict_foreach(emo, k, v) {
2658 xs *current = xs_dict_new();
2659 if ((xs_startswith(v, "https://") && xs_startswith((xs_mime_by_ext(v)), "image/")) || xs_type(v) == XSTYPE_DICT) {
2660 /* remove first and last colon */
2661 if (xs_type(v) == XSTYPE_DICT) {
2662 const char *v2;
2663 const char *cat = k;
2664 xs_dict_foreach(v, k, v2) {
2665 xs *shortcode = xs_replace(k, ":", "");
2666 current = xs_dict_append(current, "shortcode", shortcode);
2667 current = xs_dict_append(current, "url", v2);
2668 current = xs_dict_append(current, "static_url", v2);
2669 current = xs_dict_append(current, "visible_in_picker", xs_stock(XSTYPE_TRUE));
2670 current = xs_dict_append(current, "category", cat);
2671 list = xs_list_append(list, current);
2672 }
2673 }
2674 else {
2675 xs *shortcode = xs_replace(k, ":", "");
2676 current = xs_dict_append(current, "shortcode", shortcode);
2677 current = xs_dict_append(current, "url", v);
2678 current = xs_dict_append(current, "static_url", v);
2679 current = xs_dict_append(current, "visible_in_picker", xs_stock(XSTYPE_TRUE));
2680 list = xs_list_append(list, current);
2681 }
2682 }
2683 }
2684 *body = xs_json_dumps(list, 0);
2685 *ctype = "application/json";
2686 status = HTTP_STATUS_OK;
2687 }
2688 else
2689 if (strcmp(cmd, "/v1/instance") == 0) { /** **/
2690 /* returns an instance object */
2691 xs *ins = xs_dict_new();
2692 const char *host = xs_dict_get(srv_config, "host");
2693 const char *title = xs_dict_get(srv_config, "title");
2694 const char *sdesc = xs_dict_get(srv_config, "short_description");
2695
2696 ins = xs_dict_append(ins, "uri", host);
2697 ins = xs_dict_append(ins, "domain", host);
2698 ins = xs_dict_append(ins, "title", title && *title ? title : host);
2699 ins = xs_dict_append(ins, "version", "4.0.0 (not true; really " USER_AGENT ")");
2700 ins = xs_dict_append(ins, "source_url", WHAT_IS_SNAC_URL);
2701 ins = xs_dict_append(ins, "description", host);
2702
2703 ins = xs_dict_append(ins, "short_description", sdesc && *sdesc ? sdesc : host);
2704
2705 xs *susie = xs_fmt("%s/susie.png", srv_baseurl);
2706 ins = xs_dict_append(ins, "thumbnail", susie);
2707
2708 const char *v = xs_dict_get(srv_config, "admin_email");
2709 if (xs_is_null(v) || *v == '\0')
2710 v = "admin@localhost";
2711
2712 ins = xs_dict_append(ins, "email", v);
2713
2714 ins = xs_dict_append(ins, "rules", xs_stock(XSTYPE_LIST));
2715
2716 xs *l1 = xs_list_append(xs_list_new(), "en");
2717 ins = xs_dict_append(ins, "languages", l1);
2718
2719 xs *wss = xs_fmt("wss:/" "/%s", xs_dict_get(srv_config, "host"));
2720 xs *urls = xs_dict_new();
2721 urls = xs_dict_append(urls, "streaming_api", wss);
2722
2723 ins = xs_dict_append(ins, "urls", urls);
2724
2725 xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock(0));
2726 d2 = xs_dict_append(d2, "status_count", xs_stock(0));
2727 d2 = xs_dict_append(d2, "domain_count", xs_stock(0));
2728 ins = xs_dict_append(ins, "stats", d2);
2729
2730 ins = xs_dict_append(ins, "registrations", xs_stock(XSTYPE_FALSE));
2731 ins = xs_dict_append(ins, "approval_required", xs_stock(XSTYPE_FALSE));
2732 ins = xs_dict_append(ins, "invites_enabled", xs_stock(XSTYPE_FALSE));
2733
2734 xs *cfg = xs_dict_new();
2735
2736 {
2737 xs *d11 = xs_json_loads("{\"characters_reserved_per_url\":32,"
2738 "\"max_characters\":100000,\"max_media_attachments\":4}");
2739
2740 const xs_number *max_attachments = xs_dict_get(srv_config, "max_attachments");
2741 if (xs_type(max_attachments) == XSTYPE_NUMBER)
2742 d11 = xs_dict_set(d11, "max_media_attachments", max_attachments);
2743
2744 cfg = xs_dict_append(cfg, "statuses", d11);
2745
2746 xs *d12 = xs_json_loads("{\"max_featured_tags\":0}");
2747 cfg = xs_dict_append(cfg, "accounts", d12);
2748
2749 xs *d13 = xs_json_loads("{\"image_matrix_limit\":33177600,"
2750 "\"image_size_limit\":16777216,"
2751 "\"video_frame_rate_limit\":120,"
2752 "\"video_matrix_limit\":8294400,"
2753 "\"video_size_limit\":103809024}"
2754 );
2755
2756 {
2757 /* get the supported mime types from the internal list */
2758 const char **p = xs_mime_types;
2759 xs_set mtypes;
2760 xs_set_init(&mtypes);
2761
2762 while (*p) {
2763 const char *type = p[1];
2764
2765 if (xs_startswith(type, "image/") ||
2766 xs_startswith(type, "video/") ||
2767 xs_startswith(type, "audio/"))
2768 xs_set_add(&mtypes, type);
2769
2770 p += 2;
2771 }
2772
2773 xs *l = xs_set_result(&mtypes);
2774 d13 = xs_dict_append(d13, "supported_mime_types", l);
2775 }
2776
2777 cfg = xs_dict_append(cfg, "media_attachments", d13);
2778
2779 xs *d14 = xs_json_loads("{\"max_characters_per_option\":50,"
2780 "\"max_expiration\":2629746,"
2781 "\"max_options\":8,\"min_expiration\":300}");
2782 cfg = xs_dict_append(cfg, "polls", d14);
2783
2784
2785 xs *d15 = xs_json_loads("{\"max_reactions\":50}");
2786 cfg = xs_dict_append(cfg, "reactions", d15);
2787
2788 }
2789
2790 ins = xs_dict_append(ins, "configuration", cfg);
2791
2792 const char *admin_account = xs_dict_get(srv_config, "admin_account");
2793
2794 if (!xs_is_null(admin_account) && *admin_account) {
2795 snac admin;
2796
2797 if (user_open(&admin, admin_account)) {
2798 xs *actor = msg_actor(&admin);
2799 xs *acct = mastoapi_account(NULL, actor);
2800
2801 ins = xs_dict_append(ins, "contact_account", acct);
2802
2803 user_free(&admin);
2804 }
2805 }
2806
2807 *body = xs_json_dumps(ins, 4);
2808 *ctype = "application/json";
2809 status = HTTP_STATUS_OK;
2810 }
2811 else
2812 if (strcmp(cmd, "/v1/instance/peers") == 0) { /** **/
2813 /* get the collected inbox list as the instances "this domain is aware of" */
2814 xs *list = inbox_list();
2815 xs *peers = xs_list_new();
2816 const char *inbox;
2817
2818 xs_list_foreach(list, inbox) {
2819 xs *l = xs_split(inbox, "/");
2820 const char *domain = xs_list_get(l, 2);
2821
2822 if (xs_is_string(domain))
2823 peers = xs_list_append(peers, domain);
2824 }
2825
2826 *body = xs_json_dumps(peers, 4);
2827 *ctype = "application/json";
2828 status = HTTP_STATUS_OK;
2829 }
2830 else
2831 if (strcmp(cmd, "/v1/instance/extended_description") == 0) { /** **/
2832 xs *d = xs_dict_new();
2833 xs *greeting = xs_fmt("%s/greeting.html", srv_basedir);
2834 time_t t = mtime(greeting);
2835 xs *updated_at = xs_str_iso_date(t);
2836 xs *content = xs_replace(snac_blurb, "%host%", xs_dict_get(srv_config, "host"));
2837
2838 d = xs_dict_set(d, "updated_at", updated_at);
2839 d = xs_dict_set(d, "content", content);
2840
2841 *body = xs_json_dumps(d, 4);
2842 *ctype = "application/json";
2843 status = HTTP_STATUS_OK;
2844 }
2845 else
2846 if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
2847 /* information about a status */
2848 if (logged_in) {
2849 xs *l = xs_split(cmd, "/");
2850 const char *oid = xs_list_get(l, 3);
2851 const char *op = xs_list_get(l, 4);
2852
2853 if (!xs_is_null(oid)) {
2854 xs *msg = NULL;
2855 xs *out = NULL;
2856
2857 /* skip the 'fake' part of the id */
2858 oid = MID_TO_MD5(oid);
2859
2860 xs *id = xs_tolower_i(xs_dup(oid));
2861
2862 if (valid_status(object_get_by_md5(id, &msg))) {
2863 if (op == NULL) {
2864 if (!is_muted(&snac1, get_atto(msg))) {
2865 /* return the status itself */
2866 out = mastoapi_status(&snac1, msg);
2867 }
2868 }
2869 else
2870 if (strcmp(op, "context") == 0) { /** **/
2871 /* return ancestors and children */
2872 xs *anc = xs_list_new();
2873 xs *des = xs_list_new();
2874 char pid[MD5_HEX_SIZE];
2875
2876 /* build the [grand]parent list, moving up */
2877 strncpy(pid, id, sizeof(pid));
2878
2879 while (object_parent(pid, pid)) {
2880 xs *m2 = NULL;
2881
2882 if (valid_status(timeline_get_by_md5(&snac1, pid, &m2))) {
2883 xs *st = mastoapi_status(&snac1, m2);
2884
2885 if (st)
2886 anc = xs_list_insert(anc, 0, st);
2887 }
2888 else
2889 break;
2890 }
2891
2892 /* build the children list */
2893 xs *childs = build_childrens(msg, &snac1);
2894 if (xs_list_len(childs) > 0)
2895 des = xs_list_cat(des, childs);
2896
2897 out = xs_dict_new();
2898 out = xs_dict_append(out, "ancestors", anc);
2899 out = xs_dict_append(out, "descendants", des);
2900 }
2901 else
2902 if (strcmp(op, "reblogged_by") == 0 || /** **/
2903 strcmp(op, "favourited_by") == 0) { /** **/
2904 /* return the list of people who liked or boosted this */
2905 out = xs_list_new();
2906
2907 xs *l = NULL;
2908
2909 if (op[0] == 'r')
2910 l = object_announces(xs_dict_get(msg, "id"));
2911 else
2912 l = object_likes(xs_dict_get(msg, "id"));
2913
2914 xs_list *p = l;
2915 const xs_str *v;
2916
2917 while (xs_list_iter(&p, &v)) {
2918 xs *actor2 = NULL;
2919
2920 if (valid_status(object_get_by_md5(v, &actor2))) {
2921 xs *acct2 = mastoapi_account(&snac1, actor2);
2922
2923 out = xs_list_append(out, acct2);
2924 }
2925 }
2926 }
2927 else
2928 if (strcmp(op, "source") == 0) { /** **/
2929 out = xs_dict_new();
2930
2931 /* get the mastoapi status id */
2932 out = xs_dict_append(out, "id", xs_list_get(l, 3));
2933
2934 out = xs_dict_append(out, "text", xs_dict_get(msg, "sourceContent"));
2935 out = xs_dict_append(out, "spoiler_text", xs_dict_get(msg, "summary"));
2936 }
2937 }
2938 else
2939 srv_debug(1, xs_fmt("mastoapi status: bad id %s", id));
2940
2941 if (out != NULL) {
2942 *body = xs_json_dumps(out, 4);
2943 *ctype = "application/json";
2944 status = HTTP_STATUS_OK;
2945 }
2946 }
2947 }
2948 else
2949 status = HTTP_STATUS_UNAUTHORIZED;
2950 }
2951 else
2952 if (strcmp(cmd, "/v1/preferences") == 0) { /** **/
2953 /* TBD */
2954 *body = xs_dup("{}");
2955 *ctype = "application/json";
2956 status = HTTP_STATUS_OK;
2957 }
2958 else
2959 if (strcmp(cmd, "/v1/markers") == 0) { /** **/
2960 if (logged_in) {
2961 const xs_list *timeline = xs_dict_get(args, "timeline[]");
2962 xs_str *json = NULL;
2963 if (!xs_is_null(timeline))
2964 json = xs_json_dumps(markers_get(&snac1, timeline), 4);
2965
2966 if (!xs_is_null(json))
2967 *body = json;
2968 else
2969 *body = xs_dup("{}");
2970
2971 *ctype = "application/json";
2972 status = HTTP_STATUS_OK;
2973 }
2974 else
2975 status = HTTP_STATUS_UNAUTHORIZED;
2976 }
2977 else
2978 if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
2979 if (logged_in) {
2980 xs *r = xs_list_new();
2981 const xs_list *followed_hashtags = xs_dict_get_def(snac1.config,
2982 "followed_hashtags", xs_stock(XSTYPE_LIST));
2983 const char *hashtag;
2984
2985 xs_list_foreach(followed_hashtags, hashtag) {
2986 if (*hashtag == '#') {
2987 xs *d = xs_dict_new();
2988 xs *s = xs_fmt("%s?t=%s", srv_baseurl, hashtag + 1);
2989
2990 d = xs_dict_set(d, "name", hashtag + 1);
2991 d = xs_dict_set(d, "url", s);
2992 d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST));
2993
2994 r = xs_list_append(r, d);
2995 }
2996 }
2997
2998 *body = xs_json_dumps(r, 4);
2999 *ctype = "application/json";
3000 status = HTTP_STATUS_OK;
3001 }
3002 else
3003 status = HTTP_STATUS_UNAUTHORIZED;
3004 }
3005 else
3006 if (strcmp(cmd, "/v1/blocks") == 0) { /** **/
3007 *body = xs_dup("[]");
3008 *ctype = "application/json";
3009 status = HTTP_STATUS_OK;
3010 }
3011 else
3012 if (strcmp(cmd, "/v1/mutes") == 0) { /** **/
3013 *body = xs_dup("[]");
3014 *ctype = "application/json";
3015 status = HTTP_STATUS_OK;
3016 }
3017 else
3018 if (strcmp(cmd, "/v1/trends/tags") == 0) { /** **/
3019 *body = xs_dup("[]");
3020 *ctype = "application/json";
3021 status = HTTP_STATUS_OK;
3022 }
3023 else
3024 if (strcmp(cmd, "/v1/trends/statuses") == 0) { /** **/
3025 *body = xs_dup("[]");
3026 *ctype = "application/json";
3027 status = HTTP_STATUS_OK;
3028 }
3029 else
3030 if (strcmp(cmd, "/v2/search") == 0) { /** **/
3031 if (logged_in) {
3032 const char *q = xs_dict_get(args, "q");
3033 const char *type = xs_dict_get(args, "type");
3034 const char *offset = xs_dict_get(args, "offset");
3035
3036 xs *acl = xs_list_new();
3037 xs *stl = xs_list_new();
3038 xs *htl = xs_list_new();
3039 xs *res = xs_dict_new();
3040
3041 if (xs_is_null(offset) || strcmp(offset, "0") == 0) {
3042 /* reply something only for offset 0; otherwise,
3043 apps like Tusky keep asking again and again */
3044 if (xs_startswith(q, "https://")) {
3045 if (!timeline_here(&snac1, q)) {
3046 xs *object = NULL;
3047 int status;
3048
3049 status = activitypub_request(&snac1, q, &object);
3050 snac_debug(&snac1, 1, xs_fmt("Request searched URL %s %d", q, status));
3051
3052 if (valid_status(status)) {
3053 /* got it; also request the actor */
3054 const char *attr_to = get_atto(object);
3055 xs *actor_obj = NULL;
3056
3057 if (!xs_is_null(attr_to)) {
3058 status = actor_request(&snac1, attr_to, &actor_obj);
3059
3060 snac_debug(&snac1, 1, xs_fmt("Request author %s of %s %d", attr_to, q, status));
3061
3062 if (valid_status(status)) {
3063 /* add the actor */
3064 actor_add(attr_to, actor_obj);
3065
3066 /* add the post to the timeline */
3067 timeline_add(&snac1, q, object);
3068 }
3069 }
3070 }
3071 }
3072 }
3073
3074 if (!xs_is_null(q)) {
3075 if (xs_is_null(type) || strcmp(type, "accounts") == 0) {
3076 /* do a webfinger query */
3077 char *actor = NULL;
3078 char *user = NULL;
3079
3080 if (valid_status(webfinger_request(q, &actor, &user)) && actor) {
3081 xs *actor_o = NULL;
3082
3083 if (valid_status(actor_request(&snac1, actor, &actor_o))) {
3084 xs *acct = mastoapi_account(NULL, actor_o);
3085
3086 acl = xs_list_append(acl, acct);
3087
3088 if (!object_here(actor))
3089 object_add(actor, actor_o);
3090 }
3091 }
3092 }
3093
3094 if (xs_is_null(type) || strcmp(type, "hashtags") == 0) {
3095 /* search this tag */
3096 xs *tl = tag_search((char *)q, 0, 1);
3097
3098 if (xs_list_len(tl)) {
3099 xs *d = xs_dict_new();
3100
3101 d = xs_dict_append(d, "name", q);
3102 xs *url = xs_fmt("%s?t=%s", srv_baseurl, q);
3103 d = xs_dict_append(d, "url", url);
3104 d = xs_dict_append(d, "history", xs_stock(XSTYPE_LIST));
3105
3106 htl = xs_list_append(htl, d);
3107 }
3108 }
3109
3110 if (xs_is_null(type) || strcmp(type, "statuses") == 0) {
3111 int to = 0;
3112 int cnt = 40;
3113 xs *tl = content_search(&snac1, q, 1, 0, cnt, 0, &to);
3114 int c = 0;
3115 const char *v;
3116
3117 while (xs_list_next(tl, &v, &c) && --cnt) {
3118 xs *post = NULL;
3119
3120 if (!valid_status(timeline_get_by_md5(&snac1, v, &post)))
3121 continue;
3122
3123 xs *s = mastoapi_status(&snac1, post);
3124
3125 if (!xs_is_null(s))
3126 stl = xs_list_append(stl, s);
3127 }
3128 }
3129 }
3130 }
3131
3132 res = xs_dict_append(res, "accounts", acl);
3133 res = xs_dict_append(res, "statuses", stl);
3134 res = xs_dict_append(res, "hashtags", htl);
3135
3136 *body = xs_json_dumps(res, 4);
3137 *ctype = "application/json";
3138 status = HTTP_STATUS_OK;
3139 }
3140 else
3141 status = HTTP_STATUS_UNAUTHORIZED;
3142 }
3143
3144 /* user cleanup */
3145 if (logged_in)
3146 user_free(&snac1);
3147
3148 srv_debug(1, xs_fmt("mastoapi_get_handler %s %d", q_path, status));
3149
3150 return status;
3151}
3152
3153
3154int mastoapi_post_handler(const xs_dict *req, const char *q_path,
3155 const char *payload, int p_size,
3156 char **body, int *b_size, char **ctype)
3157{
3158 (void)p_size;
3159 (void)b_size;
3160
3161 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
3162 return 0;
3163
3164 int status = HTTP_STATUS_NOT_FOUND;
3165 xs *args = NULL;
3166 const char *i_ctype = xs_dict_get(req, "content-type");
3167
3168 if (i_ctype && xs_startswith(i_ctype, "application/json")) {
3169 if (!xs_is_null(payload)) {
3170 args = xs_json_loads(payload);
3171 }
3172 }
3173 else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
3174 {
3175 // Some apps send form data instead of json so we should cater for those
3176 if (!xs_is_null(payload)) {
3177 args = xs_url_vars(payload);
3178 }
3179 }
3180 else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data"))
3181 {
3182 // Handle multipart/form-data by using p_vars (already parsed by httpd)
3183 args = xs_dup(xs_dict_get(req, "p_vars"));
3184 }
3185
3186 /* if args still NULL, try falling back to p_vars or q_vars */
3187 if (args == NULL)
3188 args = xs_dup(xs_dict_get(req, "p_vars"));
3189
3190 if (args == NULL)
3191 args = xs_dup(xs_dict_get(req, "q_vars"));
3192
3193 if (args == NULL) {
3194 srv_debug(1, xs_fmt("mastoapi_post_handler: failed to parse args for %s, content-type: %s",
3195 q_path, i_ctype ? i_ctype : "(null)"));
3196 return HTTP_STATUS_BAD_REQUEST;
3197 }
3198
3199 xs *cmd = xs_replace_n(q_path, "/api", "", 1);
3200
3201 snac snac = {0};
3202 int logged_in = process_auth_token(&snac, req);
3203
3204 if (strcmp(cmd, "/v1/apps") == 0) { /** **/
3205 const char *name = xs_dict_get(args, "client_name");
3206 const char *ruri = xs_dict_get(args, "redirect_uris");
3207 const char *scope = xs_dict_get(args, "scope");
3208
3209 /* Ice Cubes sends these values as query parameters, so try these */
3210 if (name == NULL && ruri == NULL && scope == NULL) {
3211 xs_free(args);
3212 args = xs_dup(xs_dict_get(req, "q_vars"));
3213 name = xs_dict_get(args, "client_name");
3214 ruri = xs_dict_get(args, "redirect_uris");
3215 scope = xs_dict_get(args, "scope");
3216 }
3217
3218 if (xs_type(ruri) == XSTYPE_LIST)
3219 ruri = xs_dict_get(ruri, 0);
3220
3221 if (name && ruri) {
3222 xs *app = xs_dict_new();
3223 xs *id = xs_replace_i(tid(0), ".", "");
3224 xs *csec = random_str();
3225 xs *vkey = xs_dup(xs_dict_get(srv_config, "vkey"));
3226 if (vkey == NULL)
3227 vkey = random_str();
3228
3229 xs *cid = NULL;
3230
3231 /* pick a non-existent random cid */
3232 for (;;) {
3233 cid = random_str();
3234 xs *p_app = app_get(cid);
3235
3236 if (p_app == NULL)
3237 break;
3238
3239 xs_free(cid);
3240 }
3241
3242 app = xs_dict_append(app, "name", name);
3243 app = xs_dict_append(app, "redirect_uri", ruri);
3244 app = xs_dict_append(app, "client_id", cid);
3245 app = xs_dict_append(app, "client_secret", csec);
3246 app = xs_dict_append(app, "vapid_key", vkey);
3247 app = xs_dict_append(app, "id", id);
3248 app = xs_dict_append(app, "client_secret_expires_at", xs_stock(0));
3249
3250 *body = xs_json_dumps(app, 4);
3251 *ctype = "application/json";
3252 status = HTTP_STATUS_OK;
3253
3254 app = xs_dict_append(app, "code", "");
3255
3256 if (scope)
3257 app = xs_dict_append(app, "scope", scope);
3258
3259 app_add(cid, app);
3260
3261 srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
3262 }
3263 }
3264 else
3265 if (strcmp(cmd, "/v1/statuses") == 0) { /** **/
3266 if (logged_in) {
3267 /* post a new Note */
3268 const char *content = xs_dict_get(args, "status");
3269 const char *mid = xs_dict_get(args, "in_reply_to_id");
3270 const char *visibility = xs_dict_get(args, "visibility");
3271 const char *summary = xs_dict_get(args, "spoiler_text");
3272 const char *poll_opts = xs_dict_get(args, "poll[options][]");
3273 const char *poll_end_secs = xs_dict_get(args, "poll[expires_in]");
3274 const char *poll_multiple = xs_dict_get(args, "poll[multiple]");
3275 // const char *poll_hide_totals = xs_dict_get(args, "poll[hide_totals]");
3276 const char *media_ids = xs_dict_get(args, "media_ids");
3277 const char *language = xs_dict_get(args, "language");
3278
3279 if (xs_is_null(media_ids))
3280 media_ids = xs_dict_get(args, "media_ids[]");
3281
3282 if (xs_is_null(media_ids))
3283 media_ids = xs_dict_get(args, "media_ids");
3284
3285 if (xs_is_null(visibility))
3286 visibility = "public";
3287
3288 xs *attach_list = xs_list_new();
3289 xs *irt = NULL;
3290
3291 /* is it a reply? */
3292 if (mid != NULL) {
3293 xs *r_msg = NULL;
3294 const char *md5 = MID_TO_MD5(mid);
3295
3296 if (valid_status(object_get_by_md5(md5, &r_msg)))
3297 irt = xs_dup(xs_dict_get(r_msg, "id"));
3298 }
3299
3300 /* does it have attachments (and no poll)? */
3301 if (!xs_is_null(media_ids) && xs_is_null(poll_end_secs) && xs_is_null(poll_opts)) {
3302 xs *mi = NULL;
3303
3304 if (xs_type(media_ids) == XSTYPE_LIST)
3305 mi = xs_dup(media_ids);
3306 else {
3307 mi = xs_list_new();
3308 mi = xs_list_append(mi, media_ids);
3309 }
3310
3311 xs_list *p = mi;
3312 const xs_str *v;
3313
3314 while (xs_list_iter(&p, &v)) {
3315 xs *l = xs_list_new();
3316 xs *url = xs_fmt("%s/s/%s", snac.actor, v);
3317 xs *desc = static_get_meta(&snac, v);
3318
3319 l = xs_list_append(l, url);
3320 l = xs_list_append(l, desc);
3321
3322 attach_list = xs_list_append(attach_list, l);
3323 }
3324 }
3325
3326 /* prepare the message */
3327 xs *msg = NULL;
3328
3329 int scope = SCOPE_MENTIONED;
3330 if (strcmp(visibility, "unlisted") == 0)
3331 scope = SCOPE_UNLISTED;
3332 else
3333 if (strcmp(visibility, "public") == 0)
3334 scope = SCOPE_PUBLIC;
3335 else
3336 if (strcmp(visibility, "private") == 0)
3337 scope = SCOPE_FOLLOWERS;
3338
3339 /* does it have a poll? */
3340 if (!xs_is_null(poll_opts) && !xs_is_null(poll_end_secs)) {
3341 xs *po = NULL;
3342 int end_secs = atoi(poll_end_secs);
3343 int multiple = 0;
3344
3345 if (xs_type(poll_opts) == XSTYPE_LIST)
3346 po = xs_dup(poll_opts);
3347 else {
3348 po = xs_list_new();
3349 po = xs_list_append(po, poll_opts);
3350 }
3351
3352 if (!xs_is_null(poll_multiple) && strcmp(poll_multiple, "true") == 0)
3353 multiple = 1;
3354
3355 msg = msg_question(&snac, content, attach_list,
3356 poll_opts, multiple, end_secs);
3357
3358 enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
3359 }
3360 else
3361 msg = msg_note(&snac, content, NULL, irt, attach_list, scope, language, NULL);
3362
3363 if (!xs_is_null(summary) && *summary) {
3364 msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
3365 msg = xs_dict_set(msg, "summary", summary);
3366 }
3367
3368 /* scheduled? */
3369 const char *scheduled_at = xs_dict_get(args, "scheduled_at");
3370
3371 if (xs_is_string(scheduled_at) && *scheduled_at) {
3372 msg = xs_dict_set(msg, "published", scheduled_at);
3373
3374 schedule_add(&snac, xs_dict_get(msg, "id"), msg);
3375 }
3376 else {
3377 /* store */
3378 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
3379
3380 /* 'Create' message */
3381 xs *c_msg = msg_create(&snac, msg);
3382 enqueue_message(&snac, c_msg);
3383
3384 timeline_touch(&snac);
3385 }
3386
3387 /* convert to a mastodon status as a response code */
3388 xs *st = mastoapi_status(&snac, msg);
3389
3390 *body = xs_json_dumps(st, 4);
3391 *ctype = "application/json";
3392 status = HTTP_STATUS_OK;
3393 }
3394 else
3395 status = HTTP_STATUS_UNAUTHORIZED;
3396 }
3397 else
3398 if (xs_startswith(cmd, "/v1/statuses")) { /** **/
3399 if (logged_in) {
3400 /* operations on a status */
3401 xs *l = xs_split(cmd, "/");
3402 const char *mid = xs_list_get(l, 3);
3403 const char *op = xs_list_get(l, 4);
3404
3405 if (!xs_is_null(mid)) {
3406 xs *msg = NULL;
3407 xs *out = NULL;
3408
3409 /* skip the 'fake' part of the id */
3410 mid = MID_TO_MD5(mid);
3411
3412 /* try timeline first, then global object store for remote posts */
3413 int found = valid_status(timeline_get_by_md5(&snac, mid, &msg));
3414 if (!found)
3415 found = valid_status(object_get_by_md5(mid, &msg));
3416
3417 if (found) {
3418 const char *id = xs_dict_get(msg, "id");
3419
3420 if (op == NULL) {
3421 /* no operation (?) */
3422 }
3423 else
3424 if (strcmp(op, "favourite") == 0) { /** **/
3425 xs *n_msg = msg_admiration(&snac, id, "Like");
3426
3427 if (n_msg != NULL) {
3428 enqueue_message(&snac, n_msg);
3429 timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1, msg);
3430
3431 out = mastoapi_status(&snac, msg);
3432 }
3433 }
3434 else
3435 if (strcmp(op, "unfavourite") == 0) { /** **/
3436 xs *n_msg = msg_repulsion(&snac, id, "Like");
3437
3438 if (n_msg != NULL) {
3439 object_user_cache_del(&snac, id, "admire");
3440 enqueue_message(&snac, n_msg);
3441
3442 out = mastoapi_status(&snac, msg);
3443 }
3444 }
3445 else
3446 if (strcmp(op, "react") == 0) { /** **/
3447 const char *eid = xs_list_get(l, 5);
3448 xs *n_msg = msg_emoji_init(&snac, id, eid);
3449 if (n_msg)
3450 out = mastoapi_status(&snac, n_msg);
3451 }
3452 else
3453 if (strcmp(op, "unreact") == 0) { /** **/
3454 const char *eid = xs_list_get(l, 5);
3455 xs *content = xs_fmt("%s", eid);
3456
3457 if (eid) {
3458 xs *n_msg = msg_emoji_unreact(&snac, id, content);
3459
3460 if (n_msg != NULL) {
3461 object_user_cache_del(&snac, id, "admire");
3462 enqueue_message(&snac, n_msg);
3463
3464 out = mastoapi_status(&snac, msg);
3465 }
3466 }
3467 }
3468
3469 else
3470 if (strcmp(op, "reblog") == 0) { /** **/
3471 xs *n_msg = msg_admiration(&snac, id, "Announce");
3472
3473 if (n_msg != NULL) {
3474 enqueue_message(&snac, n_msg);
3475 timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0, msg);
3476
3477 out = mastoapi_status(&snac, msg);
3478 }
3479 }
3480 else
3481 if (strcmp(op, "unreblog") == 0) { /** **/
3482 xs *n_msg = msg_repulsion(&snac, id, "Announce");
3483
3484 if (n_msg != NULL) {
3485 enqueue_message(&snac, n_msg);
3486
3487 out = mastoapi_status(&snac, msg);
3488 }
3489 }
3490 else
3491 if (strcmp(op, "bookmark") == 0) { /** **/
3492 /* bookmark this message */
3493 if (bookmark(&snac, id) == 0)
3494 out = mastoapi_status(&snac, msg);
3495 else
3496 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3497 }
3498 else
3499 if (strcmp(op, "unbookmark") == 0) { /** **/
3500 /* unbookmark this message */
3501 unbookmark(&snac, id);
3502 out = mastoapi_status(&snac, msg);
3503 }
3504 else
3505 if (strcmp(op, "pin") == 0) { /** **/
3506 /* pin this message */
3507 if (pin(&snac, id) == 0)
3508 out = mastoapi_status(&snac, msg);
3509 else
3510 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3511 }
3512 else
3513 if (strcmp(op, "unpin") == 0) { /** **/
3514 /* unpin this message */
3515 unpin(&snac, id);
3516 out = mastoapi_status(&snac, msg);
3517 }
3518 else
3519 if (strcmp(op, "mute") == 0) { /** **/
3520 /* Mastodon's mute is snac's hide */
3521 }
3522 else
3523 if (strcmp(op, "unmute") == 0) { /** **/
3524 /* Mastodon's unmute is snac's unhide */
3525 }
3526 }
3527
3528 if (out != NULL) {
3529 *body = xs_json_dumps(out, 4);
3530 *ctype = "application/json";
3531 status = HTTP_STATUS_OK;
3532 }
3533 }
3534 }
3535 else
3536 status = HTTP_STATUS_UNAUTHORIZED;
3537 }
3538 else
3539 if (strcmp(cmd, "/v1/notifications/clear") == 0) { /** **/
3540 if (logged_in) {
3541 notify_clear(&snac);
3542 timeline_touch(&snac);
3543
3544 *body = xs_dup("{}");
3545 *ctype = "application/json";
3546 status = HTTP_STATUS_OK;
3547 }
3548 else
3549 status = HTTP_STATUS_UNAUTHORIZED;
3550 }
3551 else
3552 if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
3553 /* I don't know what I'm doing */
3554 if (logged_in) {
3555 const char *v;
3556
3557 xs *wpush = xs_dict_new();
3558
3559 wpush = xs_dict_append(wpush, "id", "1");
3560
3561 v = xs_dict_get(args, "data");
3562 v = xs_dict_get(v, "alerts");
3563 wpush = xs_dict_append(wpush, "alerts", v);
3564
3565 v = xs_dict_get(args, "subscription");
3566 v = xs_dict_get(v, "endpoint");
3567 wpush = xs_dict_append(wpush, "endpoint", v);
3568
3569 xs *server_key = random_str();
3570 wpush = xs_dict_append(wpush, "server_key", server_key);
3571
3572 *body = xs_json_dumps(wpush, 4);
3573 *ctype = "application/json";
3574 status = HTTP_STATUS_OK;
3575 }
3576 else
3577 status = HTTP_STATUS_UNAUTHORIZED;
3578 }
3579 else
3580 if (strcmp(cmd, "/v1/media") == 0 || strcmp(cmd, "/v2/media") == 0) { /** **/
3581 if (logged_in) {
3582 const xs_list *file = xs_dict_get(args, "file");
3583 const char *desc = xs_dict_get(args, "description");
3584
3585 if (xs_is_null(desc))
3586 desc = "";
3587
3588 status = HTTP_STATUS_BAD_REQUEST;
3589
3590 if (xs_type(file) == XSTYPE_LIST) {
3591 const char *fn = xs_list_get(file, 0);
3592
3593 if (*fn != '\0') {
3594 char *ext = strrchr(fn, '.');
3595 char rnd[32];
3596 xs_rnd_buf(rnd, sizeof(rnd));
3597 xs *hash = xs_md5_hex(rnd, sizeof(rnd));
3598 xs *id = xs_fmt("post-%s%s", hash, ext ? ext : "");
3599 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
3600 int fo = xs_number_get(xs_list_get(file, 1));
3601 int fs = xs_number_get(xs_list_get(file, 2));
3602
3603 /* store */
3604 static_put(&snac, id, payload + fo, fs);
3605 static_put_meta(&snac, id, desc);
3606
3607 /* prepare a response */
3608 xs *rsp = xs_dict_new();
3609
3610 rsp = xs_dict_append(rsp, "id", id);
3611 rsp = xs_dict_append(rsp, "type", "image");
3612 rsp = xs_dict_append(rsp, "url", url);
3613 rsp = xs_dict_append(rsp, "preview_url", url);
3614 rsp = xs_dict_append(rsp, "remote_url", url);
3615 rsp = xs_dict_append(rsp, "description", desc);
3616
3617 *body = xs_json_dumps(rsp, 4);
3618 *ctype = "application/json";
3619 status = HTTP_STATUS_OK;
3620 }
3621 }
3622 }
3623 else
3624 status = HTTP_STATUS_UNAUTHORIZED;
3625 }
3626 else
3627 if (xs_startswith(cmd, "/v1/accounts")) { /** **/
3628 if (logged_in) {
3629 /* account-related information */
3630 xs *l = xs_split(cmd, "/");
3631 const char *md5 = xs_list_get(l, 3);
3632 const char *opt = xs_list_get(l, 4);
3633 xs *rsp = NULL;
3634
3635 if (!xs_is_null(md5) && *md5) {
3636 xs *actor_o = NULL;
3637
3638 if (xs_is_null(opt)) {
3639 /* ? */
3640 }
3641 else
3642 if (strcmp(opt, "follow") == 0) { /** **/
3643 if (valid_status(object_get_by_md5(md5, &actor_o))) {
3644 const char *actor = xs_dict_get(actor_o, "id");
3645
3646 xs *msg = msg_follow(&snac, actor);
3647
3648 if (msg != NULL) {
3649 /* reload the actor from the message, in may be different */
3650 actor = xs_dict_get(msg, "object");
3651
3652 following_add(&snac, actor, msg);
3653
3654 enqueue_output_by_actor(&snac, msg, actor, 0);
3655
3656 rsp = mastoapi_relationship(&snac, md5);
3657 }
3658 }
3659 }
3660 else
3661 if (strcmp(opt, "unfollow") == 0) { /** **/
3662 if (valid_status(object_get_by_md5(md5, &actor_o))) {
3663 const char *actor = xs_dict_get(actor_o, "id");
3664
3665 /* get the following object */
3666 xs *object = NULL;
3667
3668 if (valid_status(following_get(&snac, actor, &object))) {
3669 xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
3670
3671 following_del(&snac, actor);
3672
3673 enqueue_output_by_actor(&snac, msg, actor, 0);
3674
3675 rsp = mastoapi_relationship(&snac, md5);
3676 }
3677 }
3678 }
3679 else
3680 if (strcmp(opt, "block") == 0) { /** **/
3681 if (valid_status(object_get_by_md5(md5, &actor_o))) {
3682 const char *actor = xs_dict_get(actor_o, "id");
3683
3684 mute(&snac, actor);
3685
3686 rsp = mastoapi_relationship(&snac, md5);
3687 }
3688 }
3689 else
3690 if (strcmp(opt, "unblock") == 0) { /** **/
3691 if (valid_status(object_get_by_md5(md5, &actor_o))) {
3692 const char *actor = xs_dict_get(actor_o, "id");
3693
3694 unmute(&snac, actor);
3695
3696 rsp = mastoapi_relationship(&snac, md5);
3697 }
3698 }
3699 }
3700
3701 if (rsp != NULL) {
3702 *body = xs_json_dumps(rsp, 4);
3703 *ctype = "application/json";
3704 status = HTTP_STATUS_OK;
3705 }
3706 }
3707 else
3708 status = HTTP_STATUS_UNAUTHORIZED;
3709 }
3710 else
3711 if (xs_startswith(cmd, "/v1/polls")) { /** **/
3712 if (logged_in) {
3713 /* operations on a status */
3714 xs *l = xs_split(cmd, "/");
3715 const char *mid = xs_list_get(l, 3);
3716 const char *op = xs_list_get(l, 4);
3717
3718 if (!xs_is_null(mid)) {
3719 xs *msg = NULL;
3720 xs *out = NULL;
3721
3722 /* skip the 'fake' part of the id */
3723 mid = MID_TO_MD5(mid);
3724
3725 /* try timeline first, then global object store for remote posts */
3726 int found = valid_status(timeline_get_by_md5(&snac, mid, &msg));
3727 if (!found)
3728 found = valid_status(object_get_by_md5(mid, &msg));
3729
3730 if (found) {
3731 const char *id = xs_dict_get(msg, "id");
3732 const char *atto = get_atto(msg);
3733 int closed = 0;
3734 const char *f_closed = NULL;
3735
3736 if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
3737 /* it has a closed date... but is it in the past? */
3738 time_t t0 = time(NULL);
3739 time_t t1 = xs_parse_iso_date(f_closed, 0);
3740
3741 if (t1 < t0)
3742 closed = 1;
3743 }
3744
3745 if (closed || was_question_voted(&snac, id))
3746 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3747 else {
3748 const xs_list *opts = xs_dict_get(msg, "oneOf");
3749 if (opts == NULL)
3750 opts = xs_dict_get(msg, "anyOf");
3751
3752 if (op == NULL) {
3753 }
3754 else {
3755 if (strcmp(op, "votes") == 0) {
3756 const xs_list *choices = xs_dict_get(args, "choices[]");
3757
3758 if (xs_is_null(choices))
3759 choices = xs_dict_get(args, "choices");
3760
3761 if (xs_type(choices) == XSTYPE_LIST) {
3762 const xs_str *v;
3763
3764 int c = 0;
3765 while (xs_list_next(choices, &v, &c)) {
3766 int io = atoi(v);
3767 const xs_dict *o = xs_list_get(opts, io);
3768
3769 if (o) {
3770 const char *name = xs_dict_get(o, "name");
3771
3772 xs *msg = msg_note(&snac, "", atto, (char *)id, NULL, 1, NULL, NULL);
3773 msg = xs_dict_append(msg, "name", name);
3774
3775 xs *c_msg = msg_create(&snac, msg);
3776 enqueue_message(&snac, c_msg);
3777 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
3778 }
3779 }
3780
3781 out = mastoapi_poll(&snac, msg);
3782 }
3783 }
3784 }
3785 }
3786 }
3787
3788 if (out != NULL) {
3789 *body = xs_json_dumps(out, 4);
3790 *ctype = "application/json";
3791 status = HTTP_STATUS_OK;
3792 }
3793 }
3794 }
3795 else
3796 status = HTTP_STATUS_UNAUTHORIZED;
3797 }
3798 else
3799 if (strcmp(cmd, "/v1/lists") == 0) {
3800 if (logged_in) {
3801 const char *title = xs_dict_get(args, "title");
3802
3803 if (xs_type(title) == XSTYPE_STRING) {
3804 /* add the list */
3805 xs *out = xs_dict_new();
3806 xs *lid = list_maint(&snac, title, 1);
3807
3808 if (!xs_is_null(lid)) {
3809 out = xs_dict_append(out, "id", lid);
3810 out = xs_dict_append(out, "title", title);
3811 out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
3812 out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
3813
3814 status = HTTP_STATUS_OK;
3815 }
3816 else {
3817 out = xs_dict_append(out, "error", "cannot create list");
3818 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3819 }
3820
3821 *body = xs_json_dumps(out, 4);
3822 *ctype = "application/json";
3823 }
3824 else
3825 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3826 }
3827 }
3828 else
3829 if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
3830 if (logged_in) {
3831 xs *l = xs_split(cmd, "/");
3832 const char *op = xs_list_get(l, -1);
3833 const char *id = xs_list_get(l, -2);
3834
3835 if (op && id && xs_is_hex(id)) {
3836 if (strcmp(op, "accounts") == 0) {
3837 const xs_list *accts = xs_dict_get(args, "account_ids[]");
3838
3839 if (xs_is_null(accts))
3840 accts = xs_dict_get(args, "account_ids");
3841
3842 int c = 0;
3843 const char *v;
3844
3845 while (xs_list_next(accts, &v, &c)) {
3846 list_members(&snac, id, v, 1);
3847 }
3848
3849 xs *out = xs_dict_new();
3850 *body = xs_json_dumps(out, 4);
3851 *ctype = "application/json";
3852 status = HTTP_STATUS_OK;
3853 }
3854 }
3855 }
3856 }
3857 else if (strcmp(cmd, "/v1/markers") == 0) { /** **/
3858 xs_str *json = NULL;
3859 if (logged_in) {
3860 const xs_str *home_marker = xs_dict_get(args, "home[last_read_id]");
3861 if (xs_is_null(home_marker)) {
3862 const xs_dict *home = xs_dict_get(args, "home");
3863 if (!xs_is_null(home))
3864 home_marker = xs_dict_get(home, "last_read_id");
3865 }
3866
3867 const xs_str *notify_marker = xs_dict_get(args, "notifications[last_read_id]");
3868 if (xs_is_null(notify_marker)) {
3869 const xs_dict *notify = xs_dict_get(args, "notifications");
3870 if (!xs_is_null(notify))
3871 notify_marker = xs_dict_get(notify, "last_read_id");
3872 }
3873 json = xs_json_dumps(markers_set(&snac, home_marker, notify_marker), 4);
3874 }
3875 if (!xs_is_null(json))
3876 *body = json;
3877 else
3878 *body = xs_dup("{}");
3879
3880 *ctype = "application/json";
3881 status = HTTP_STATUS_OK;
3882 }
3883 else
3884 if (xs_startswith(cmd, "/v1/follow_requests")) { /** **/
3885 if (logged_in) {
3886 /* "authorize" or "reject" */
3887 xs *rel = NULL;
3888 xs *l = xs_split(cmd, "/");
3889 const char *md5 = xs_list_get(l, -2);
3890 const char *s_cmd = xs_list_get(l, -1);
3891
3892 if (xs_is_string(md5) && xs_is_string(s_cmd)) {
3893 xs *actor = NULL;
3894
3895 if (valid_status(object_get_by_md5(md5, &actor))) {
3896 const char *actor_id = xs_dict_get(actor, "id");
3897
3898 if (strcmp(s_cmd, "authorize") == 0) {
3899 xs *fwreq = pending_get(&snac, actor_id);
3900
3901 if (fwreq != NULL) {
3902 xs *reply = msg_accept(&snac, fwreq, actor_id);
3903
3904 enqueue_message(&snac, reply);
3905
3906 if (xs_is_null(xs_dict_get(fwreq, "published"))) {
3907 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3908 fwreq = xs_dict_set(fwreq, "published", date);
3909 }
3910
3911 timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
3912
3913 follower_add(&snac, actor_id);
3914
3915 pending_del(&snac, actor_id);
3916 rel = mastoapi_relationship(&snac, md5);
3917 }
3918 }
3919 else
3920 if (strcmp(s_cmd, "reject") == 0) {
3921 pending_del(&snac, actor_id);
3922 rel = mastoapi_relationship(&snac, md5);
3923 }
3924 }
3925 }
3926
3927 if (rel != NULL) {
3928 *body = xs_json_dumps(rel, 4);
3929 *ctype = "application/json";
3930 status = HTTP_STATUS_OK;
3931 }
3932 }
3933 }
3934 else
3935 if (xs_startswith(cmd, "/v1/tags/")) { /** **/
3936 if (logged_in) {
3937 xs *l = xs_split(cmd, "/");
3938 const char *i_tag = xs_list_get(l, 3);
3939 const char *cmd = xs_list_get(l, 4);
3940
3941 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3942
3943 if (xs_is_string(i_tag) && xs_is_string(cmd)) {
3944 int ok = 0;
3945
3946 xs *tag = xs_fmt("#%s", i_tag);
3947 xs *followed_hashtags = xs_dup(xs_dict_get_def(snac.config,
3948 "followed_hashtags", xs_stock(XSTYPE_LIST)));
3949
3950 if (strcmp(cmd, "follow") == 0) {
3951 followed_hashtags = xs_list_append(followed_hashtags, tag);
3952 ok = 1;
3953 }
3954 else
3955 if (strcmp(cmd, "unfollow") == 0) {
3956 int off = xs_list_in(followed_hashtags, tag);
3957
3958 if (off != -1)
3959 followed_hashtags = xs_list_del(followed_hashtags, off);
3960
3961 ok = 1;
3962 }
3963
3964 if (ok) {
3965 /* update */
3966 xs_dict_set(snac.config, "followed_hashtags", followed_hashtags);
3967 user_persist(&snac, 0);
3968
3969 xs *d = xs_dict_new();
3970 xs *s = xs_fmt("%s?t=%s", srv_baseurl, i_tag);
3971 d = xs_dict_set(d, "name", i_tag);
3972 d = xs_dict_set(d, "url", s);
3973 d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST));
3974
3975 *body = xs_json_dumps(d, 4);
3976 *ctype = "application/json";
3977 status = HTTP_STATUS_OK;
3978 }
3979 }
3980 }
3981 }
3982 else
3983 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
3984
3985 /* user cleanup */
3986 if (logged_in)
3987 user_free(&snac);
3988
3989 srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status));
3990
3991 return status;
3992}
3993
3994
3995int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
3996 const char *payload, int p_size,
3997 char **body, int *b_size, char **ctype)
3998{
3999 (void)p_size;
4000 (void)b_size;
4001 (void)ctype;
4002
4003 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
4004 return 0;
4005
4006 int status = HTTP_STATUS_NOT_FOUND;
4007 xs *args = NULL;
4008 const char *i_ctype = xs_dict_get(req, "content-type");
4009
4010 if (i_ctype && xs_startswith(i_ctype, "application/json")) {
4011 if (!xs_is_null(payload))
4012 args = xs_json_loads(payload);
4013 }
4014 else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
4015 {
4016 // Some apps send form data instead of json so we should cater for those
4017 if (!xs_is_null(payload)) {
4018 args = xs_url_vars(payload);
4019 }
4020 }
4021 else
4022 args = xs_dup(xs_dict_get(req, "p_vars"));
4023
4024 if (args == NULL)
4025 return HTTP_STATUS_BAD_REQUEST;
4026
4027 snac snac = {0};
4028 int logged_in = process_auth_token(&snac, req);
4029
4030 xs *cmd = xs_replace_n(q_path, "/api", "", 1);
4031
4032 if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
4033 // pretend we deleted it, since it doesn't exist anyway
4034 status = HTTP_STATUS_OK;
4035 }
4036 else
4037 if (xs_startswith(cmd, "/v1/lists/")) {
4038 if (logged_in) {
4039 xs *l = xs_split(cmd, "/");
4040 const char *p = xs_list_get(l, -1);
4041
4042 if (p) {
4043 if (strcmp(p, "accounts") == 0) {
4044 /* delete account from list */
4045 p = xs_list_get(l, -2);
4046 const xs_list *accts = xs_dict_get(args, "account_ids[]");
4047
4048 if (xs_is_null(accts))
4049 accts = xs_dict_get(args, "account_ids");
4050
4051 int c = 0;
4052 const char *v;
4053
4054 while (xs_list_next(accts, &v, &c)) {
4055 list_members(&snac, p, v, 2);
4056 }
4057 }
4058 else {
4059 /* delete list */
4060 if (xs_is_hex(p)) {
4061 list_maint(&snac, p, 2);
4062 }
4063 }
4064 }
4065
4066 *ctype = "application/json";
4067 status = HTTP_STATUS_OK;
4068 }
4069 else
4070 status = HTTP_STATUS_UNAUTHORIZED;
4071 }
4072 else
4073 if (xs_startswith(cmd, "/v1/statuses/")) {
4074 if (logged_in) {
4075 xs *l = xs_split(cmd, "/");
4076 const char *p = xs_list_get(l, -1);
4077 p = MID_TO_MD5(p);
4078
4079 xs *obj = NULL;
4080 if (valid_status(object_get_by_md5(p, &obj))) {
4081 const char *id = xs_dict_get(obj, "id");
4082
4083 if (xs_is_string(id) && is_msg_mine(&snac, id)) {
4084 xs *out = mastoapi_status(&snac, obj);
4085
4086 xs *msg = msg_delete(&snac, id);
4087
4088 enqueue_message(&snac, msg);
4089
4090 timeline_del(&snac, id);
4091
4092 draft_del(&snac, id);
4093
4094 schedule_del(&snac, id);
4095
4096 snac_log(&snac, xs_fmt("deleted entry %s", id));
4097
4098 *body = xs_json_dumps(out, 4);
4099 *ctype = "application/json";
4100 status = HTTP_STATUS_OK;
4101 }
4102 }
4103 }
4104 else
4105 status = HTTP_STATUS_UNAUTHORIZED;
4106 }
4107
4108 /* user cleanup */
4109 if (logged_in)
4110 user_free(&snac);
4111
4112 srv_debug(1, xs_fmt("mastoapi_delete_handler %s %d", q_path, status));
4113
4114 return status;
4115}
4116
4117
4118int mastoapi_put_handler(const xs_dict *req, const char *q_path,
4119 const char *payload, int p_size,
4120 char **body, int *b_size, char **ctype)
4121{
4122 (void)p_size;
4123 (void)b_size;
4124
4125 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
4126 return 0;
4127
4128 int status = HTTP_STATUS_NOT_FOUND;
4129 xs *args = NULL;
4130 const char *i_ctype = xs_dict_get(req, "content-type");
4131
4132 if (i_ctype && xs_startswith(i_ctype, "application/json")) {
4133 if (!xs_is_null(payload))
4134 args = xs_json_loads(payload);
4135 }
4136 else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
4137 {
4138 // Some apps send form data instead of json so we should cater for those
4139 if (!xs_is_null(payload)) {
4140 args = xs_url_vars(payload);
4141 }
4142 }
4143 else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data"))
4144 {
4145 // Handle multipart/form-data by using p_vars (already parsed by httpd)
4146 args = xs_dup(xs_dict_get(req, "p_vars"));
4147 }
4148
4149 /* if args still NULL, try falling back to p_vars or q_vars */
4150 if (args == NULL)
4151 args = xs_dup(xs_dict_get(req, "p_vars"));
4152
4153 if (args == NULL)
4154 args = xs_dup(xs_dict_get(req, "q_vars"));
4155
4156 if (args == NULL) {
4157 srv_debug(1, xs_fmt("mastoapi_put_handler: failed to parse args for %s, content-type: %s",
4158 q_path, i_ctype ? i_ctype : "(null)"));
4159 return HTTP_STATUS_BAD_REQUEST;
4160 }
4161
4162 xs *cmd = xs_replace_n(q_path, "/api", "", 1);
4163
4164 snac snac = {0};
4165 int logged_in = process_auth_token(&snac, req);
4166
4167 if (xs_startswith(cmd, "/v1/media") || xs_startswith(cmd, "/v2/media")) { /** **/
4168 if (logged_in) {
4169 xs *l = xs_split(cmd, "/");
4170 const char *stid = xs_list_get(l, 3);
4171
4172 if (!xs_is_null(stid)) {
4173 const char *desc = xs_dict_get(args, "description");
4174
4175 /* set the image metadata */
4176 static_put_meta(&snac, stid, desc);
4177
4178 /* prepare a response */
4179 xs *rsp = xs_dict_new();
4180 xs *url = xs_fmt("%s/s/%s", snac.actor, stid);
4181
4182 rsp = xs_dict_append(rsp, "id", stid);
4183 rsp = xs_dict_append(rsp, "type", "image");
4184 rsp = xs_dict_append(rsp, "url", url);
4185 rsp = xs_dict_append(rsp, "preview_url", url);
4186 rsp = xs_dict_append(rsp, "remote_url", url);
4187 rsp = xs_dict_append(rsp, "description", desc);
4188
4189 *body = xs_json_dumps(rsp, 4);
4190 *ctype = "application/json";
4191 status = HTTP_STATUS_OK;
4192 }
4193 }
4194 else
4195 status = HTTP_STATUS_UNAUTHORIZED;
4196 }
4197 else
4198 if (xs_startswith(cmd, "/v1/statuses")) {
4199 if (logged_in) {
4200 xs *l = xs_split(cmd, "/");
4201 const char *mid = xs_list_get(l, 3);
4202
4203 if (!xs_is_null(mid)) {
4204 const char *md5 = MID_TO_MD5(mid);
4205 xs *rsp = NULL;
4206 xs *msg = NULL;
4207
4208 if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) {
4209 const char *content = xs_dict_get(args, "status");
4210 xs *atls = xs_list_new();
4211 xs *tag = xs_list_new();
4212 xs *f_content0 = not_really_markdown(content, &atls, &tag);
4213 xs *f_content = process_tags(&snac, f_content0, &tag);
4214
4215 /* replace fields with new content */
4216 msg = xs_dict_set(msg, "sourceContent", content);
4217 msg = xs_dict_set(msg, "content", f_content);
4218
4219 xs *content_map = xs_dup(xs_dict_get(msg, "contentMap"));
4220 if (xs_is_dict(content_map)) {
4221 /* get the first pair */
4222 const char *k, *v;
4223 int c = 0;
4224
4225 if (xs_dict_next(content_map, &k, &v, &c)) {
4226 content_map = xs_dict_set(content_map, k, f_content);
4227 msg = xs_dict_set(msg, "contentMap", content_map);
4228 }
4229 }
4230
4231 xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
4232 msg = xs_dict_set(msg, "updated", updated);
4233
4234 msg = xs_dict_set(msg, "tag", tag);
4235
4236 /* overwrite object, not updating the indexes */
4237 const char *id = xs_dict_get(msg, "id");
4238
4239 object_add_ow(id, msg);
4240
4241 tag_index(id, msg);
4242
4243 /* update message */
4244 xs *c_msg = msg_update(&snac, msg);
4245
4246 enqueue_message(&snac, c_msg);
4247
4248 rsp = mastoapi_status(&snac, msg);
4249 }
4250
4251 if (rsp != NULL) {
4252 *body = xs_json_dumps(rsp, 4);
4253 *ctype = "application/json";
4254 status = HTTP_STATUS_OK;
4255 }
4256 }
4257 }
4258 else
4259 status = HTTP_STATUS_UNAUTHORIZED;
4260 }
4261
4262 /* user cleanup */
4263 if (logged_in)
4264 user_free(&snac);
4265
4266 srv_debug(1, xs_fmt("mastoapi_put_handler %s %d", q_path, status));
4267
4268 return status;
4269}
4270
4271void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac)
4272/* Store header or avatar */
4273{
4274 if (data != NULL) {
4275 if (xs_type(data) == XSTYPE_LIST) {
4276 const char *fn = xs_list_get(data, 0);
4277
4278 if (fn && *fn) {
4279 const char *ext = strrchr(fn, '.');
4280
4281 /* Mona iOS sends always jpg as application/octet-stream with no filename */
4282 if (ext == NULL || strcmp(fn, key) == 0) {
4283 ext = ".jpg";
4284 }
4285
4286 /* Make sure we have a unique file name, otherwise updated images will not be
4287 * re-loaded by clients. */
4288 xs *rnd = random_str();
4289 xs *hash = xs_md5_hex(rnd, strlen(rnd));
4290 xs *id = xs_fmt("%s%s", hash, ext);
4291 xs *url = xs_fmt("%s/s/%s", snac->actor, id);
4292 int fo = xs_number_get(xs_list_get(data, 1));
4293 int fs = xs_number_get(xs_list_get(data, 2));
4294
4295 /* store */
4296 static_put(snac, id, payload + fo, fs);
4297
4298 snac->config = xs_dict_set(snac->config, key, url);
4299 }
4300 }
4301 }
4302}
4303
4304int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
4305 const char *payload, int p_size,
4306 char **body, int *b_size, char **ctype)
4307/* Handle profile updates */
4308{
4309 (void)p_size;
4310 (void)b_size;
4311
4312 if (!xs_startswith(q_path, "/api/v1/"))
4313 return 0;
4314
4315 int status = HTTP_STATUS_NOT_FOUND;
4316 xs *args = NULL;
4317 const char *i_ctype = xs_dict_get(req, "content-type");
4318
4319 if (i_ctype && xs_startswith(i_ctype, "application/json")) {
4320 if (!xs_is_null(payload))
4321 args = xs_json_loads(payload);
4322 }
4323 else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
4324 {
4325 // Some apps send form data instead of json so we should cater for those
4326 if (!xs_is_null(payload)) {
4327 args = xs_url_vars(payload);
4328 }
4329 }
4330 else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data"))
4331 {
4332 // Handle multipart/form-data by using p_vars (already parsed by httpd)
4333 args = xs_dup(xs_dict_get(req, "p_vars"));
4334 }
4335
4336 /* if args still NULL, try falling back to p_vars or q_vars */
4337 if (args == NULL)
4338 args = xs_dup(xs_dict_get(req, "p_vars"));
4339
4340 if (args == NULL)
4341 args = xs_dup(xs_dict_get(req, "q_vars"));
4342
4343 if (args == NULL) {
4344 srv_debug(1, xs_fmt("mastoapi_patch_handler: failed to parse args for %s, content-type: %s",
4345 q_path, i_ctype ? i_ctype : "(null)"));
4346 return HTTP_STATUS_BAD_REQUEST;
4347 }
4348
4349 xs *cmd = xs_replace_n(q_path, "/api", "", 1);
4350
4351 snac snac = {0};
4352 int logged_in = process_auth_token(&snac, req);
4353
4354 if (xs_startswith(cmd, "/v1/accounts/update_credentials")) { /** **/
4355 /* Update user profile fields */
4356 if (logged_in) {
4357 int c = 0;
4358 const xs_str *k;
4359 const xs_val *v;
4360 const xs_str *field_name = NULL;
4361 xs *new_fields = xs_dict_new();
4362 while (xs_dict_next(args, &k, &v, &c)) {
4363 if (strcmp(k, "display_name") == 0) {
4364 if (v != NULL)
4365 snac.config = xs_dict_set(snac.config, "name", v);
4366 }
4367 else
4368 if (strcmp(k, "note") == 0) {
4369 if (v != NULL)
4370 snac.config = xs_dict_set(snac.config, "bio", v);
4371 }
4372 else
4373 if (strcmp(k, "bot") == 0) {
4374 if (v != NULL)
4375 snac.config = xs_dict_set(snac.config, "bot",
4376 (strcmp(v, "true") == 0 ||
4377 strcmp(v, "1") == 0) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
4378 }
4379 else
4380 if (strcmp(k, "source[sensitive]") == 0) {
4381 if (v != NULL)
4382 snac.config = xs_dict_set(snac.config, "cw",
4383 strcmp(v, "true") == 0 ? "open" : "");
4384 }
4385 else
4386 if (strcmp(k, "source[privacy]") == 0) {
4387 if (v != NULL)
4388 snac.config = xs_dict_set(snac.config, "private",
4389 strcmp(v, "private") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
4390 }
4391 else
4392 if (strcmp(k, "header") == 0) {
4393 persist_image("header", v, payload, &snac);
4394 }
4395 else
4396 if (strcmp(k, "avatar") == 0) {
4397 persist_image("avatar", v, payload, &snac);
4398 }
4399 else
4400 if (xs_between("fields_attributes", k, "[name]")) {
4401 field_name = strcmp(v, "") != 0 ? v : NULL;
4402 }
4403 else
4404 if (xs_between("fields_attributes", k, "[value]")) {
4405 if (field_name != NULL) {
4406 new_fields = xs_dict_set(new_fields, field_name, v);
4407 snac.config = xs_dict_set(snac.config, "metadata", new_fields);
4408 }
4409 }
4410 else
4411 if (strcmp(k, "locked") == 0) {
4412 snac.config = xs_dict_set(snac.config, "approve_followers",
4413 xs_stock(strcmp(v, "true") == 0 ? XSTYPE_TRUE : XSTYPE_FALSE));
4414 }
4415 /* we don't have support for the following options, yet
4416 - discoverable (0/1)
4417 */
4418 }
4419
4420 /* Persist profile */
4421 if (user_persist(&snac, 1) == 0)
4422 credentials_get(body, ctype, &status, snac);
4423 else
4424 status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
4425 }
4426 else
4427 status = HTTP_STATUS_UNAUTHORIZED;
4428 }
4429
4430 /* user cleanup */
4431 if (logged_in)
4432 user_free(&snac);
4433
4434 srv_debug(1, xs_fmt("mastoapi_patch_handler %s %d", q_path, status));
4435
4436 return status;
4437}
4438
4439
4440void mastoapi_purge(void)
4441{
4442 xs *spec = xs_fmt("%s/app/" "*.json", srv_basedir);
4443 xs *files = xs_glob(spec, 1, 0);
4444 xs_list *p = files;
4445 const xs_str *v;
4446
4447 time_t mt = time(NULL) - 3600;
4448
4449 while (xs_list_iter(&p, &v)) {
4450 xs *cid = xs_replace(v, ".json", "");
4451 xs *fn = _app_fn(cid);
4452
4453 if (mtime(fn) < mt) {
4454 /* get the app */
4455 xs *app = app_get(cid);
4456
4457 if (app) {
4458 /* old apps with no uid are incomplete cruft */
4459 const char *uid = xs_dict_get(app, "uid");
4460
4461 if (xs_is_null(uid) || *uid == '\0') {
4462 unlink(fn);
4463 srv_debug(2, xs_fmt("purged %s", fn));
4464 }
4465 }
4466 }
4467 }
4468}
4469
4470
4471#endif /* #ifndef NO_MASTODON_API */