summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--html.c9
-rw-r--r--httpd.c14
-rw-r--r--mastoapi.c814
-rw-r--r--snac.h11
-rw-r--r--xs_encdec.h13
-rw-r--r--xs_version.h2
7 files changed, 860 insertions, 5 deletions
diff --git a/Makefile b/Makefile
index 3583001..05ddfc7 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ CFLAGS?=-g -Wall
5all: snac 5all: snac
6 6
7snac: snac.o main.o data.o http.o httpd.o webfinger.o \ 7snac: snac.o main.o data.o http.o httpd.o webfinger.o \
8 activitypub.o html.o utils.o format.o upgrade.o 8 activitypub.o html.o utils.o format.o upgrade.o mastoapi.o
9 $(CC) $(CFLAGS) -L/usr/local/lib *.o -lcurl -lcrypto -pthread $(LDFLAGS) -o $@ 9 $(CC) $(CFLAGS) -L/usr/local/lib *.o -lcurl -lcrypto -pthread $(LDFLAGS) -o $@
10 10
11.c.o: 11.c.o:
diff --git a/html.c b/html.c
index 9850dfd..c8d3e39 100644
--- a/html.c
+++ b/html.c
@@ -332,7 +332,7 @@ d_char *html_top_controls(snac *snac, d_char *s)
332 "<input type=\"hidden\" name=\"in_reply_to\" value=\"\">\n" 332 "<input type=\"hidden\" name=\"in_reply_to\" value=\"\">\n"
333 "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n" 333 "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n"
334 "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n" 334 "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n"
335 "<p><input type=\"file\" name=\"attach\">\n" 335 "<p>%s: <input type=\"file\" name=\"attach\">\n"
336 "<p>%s: <input type=\"text\" name=\"alt_text\">\n" 336 "<p>%s: <input type=\"text\" name=\"alt_text\">\n"
337 "<p><input type=\"submit\" class=\"button\" value=\"%s\">\n" 337 "<p><input type=\"submit\" class=\"button\" value=\"%s\">\n"
338 "</form><p>\n" 338 "</form><p>\n"
@@ -425,6 +425,7 @@ d_char *html_top_controls(snac *snac, d_char *s)
425 snac->actor, 425 snac->actor,
426 L("Sensitive content"), 426 L("Sensitive content"),
427 L("Only for mentioned people"), 427 L("Only for mentioned people"),
428 L("Image"),
428 L("Image description"), 429 L("Image description"),
429 L("Post"), 430 L("Post"),
430 431
@@ -593,7 +594,7 @@ d_char *html_entry_controls(snac *snac, d_char *os, char *msg, const char *md5)
593 594
594 "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n" 595 "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n"
595 "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n" 596 "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n"
596 "<p><input type=\"file\" name=\"attach\">\n" 597 "<p>%s: <input type=\"file\" name=\"attach\">\n"
597 "<p>%s: <input type=\"text\" name=\"alt_text\">\n" 598 "<p>%s: <input type=\"text\" name=\"alt_text\">\n"
598 599
599 "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n" 600 "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n"
@@ -609,6 +610,7 @@ d_char *html_entry_controls(snac *snac, d_char *os, char *msg, const char *md5)
609 id, 610 id,
610 L("Sensitive content"), 611 L("Sensitive content"),
611 L("Only for mentioned people"), 612 L("Only for mentioned people"),
613 L("Image"),
612 L("Image description"), 614 L("Image description"),
613 md5, 615 md5,
614 L("Post") 616 L("Post")
@@ -632,7 +634,7 @@ d_char *html_entry_controls(snac *snac, d_char *os, char *msg, const char *md5)
632 634
633 "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n" 635 "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n"
634 "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n" 636 "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n"
635 "<p><input type=\"file\" name=\"attach\">\n" 637 "<p>%s: <input type=\"file\" name=\"attach\">\n"
636 "<p>%s: <input type=\"text\" name=\"alt_text\">\n" 638 "<p>%s: <input type=\"text\" name=\"alt_text\">\n"
637 639
638 "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n" 640 "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n"
@@ -648,6 +650,7 @@ d_char *html_entry_controls(snac *snac, d_char *os, char *msg, const char *md5)
648 id, 650 id,
649 L("Sensitive content"), 651 L("Sensitive content"),
650 L("Only for mentioned people"), 652 L("Only for mentioned people"),
653 L("Image"),
651 L("Image description"), 654 L("Image description"),
652 md5, 655 md5,
653 L("Post") 656 L("Post")
diff --git a/httpd.c b/httpd.c
index ec7a79f..d5de87c 100644
--- a/httpd.c
+++ b/httpd.c
@@ -177,6 +177,12 @@ void httpd_connection(FILE *f)
177 status = activitypub_get_handler(req, q_path, &body, &b_size, &ctype); 177 status = activitypub_get_handler(req, q_path, &body, &b_size, &ctype);
178 178
179 if (status == 0) 179 if (status == 0)
180 status = oauth_get_handler(req, q_path, &body, &b_size, &ctype);
181
182 if (status == 0)
183 status = mastoapi_get_handler(req, q_path, &body, &b_size, &ctype);
184
185 if (status == 0)
180 status = html_get_handler(req, q_path, &body, &b_size, &ctype); 186 status = html_get_handler(req, q_path, &body, &b_size, &ctype);
181 } 187 }
182 else 188 else
@@ -186,6 +192,14 @@ void httpd_connection(FILE *f)
186 payload, p_size, &body, &b_size, &ctype); 192 payload, p_size, &body, &b_size, &ctype);
187 193
188 if (status == 0) 194 if (status == 0)
195 status = oauth_post_handler(req, q_path,
196 payload, p_size, &body, &b_size, &ctype);
197
198 if (status == 0)
199 status = mastoapi_post_handler(req, q_path,
200 payload, p_size, &body, &b_size, &ctype);
201
202 if (status == 0)
189 status = html_post_handler(req, q_path, 203 status = html_post_handler(req, q_path,
190 payload, p_size, &body, &b_size, &ctype); 204 payload, p_size, &body, &b_size, &ctype);
191 } 205 }
diff --git a/mastoapi.c b/mastoapi.c
new file mode 100644
index 0000000..b223ce4
--- /dev/null
+++ b/mastoapi.c
@@ -0,0 +1,814 @@
1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2023 grunfink / MIT license */
3
4#include "xs.h"
5#include "xs_encdec.h"
6#include "xs_openssl.h"
7#include "xs_json.h"
8#include "xs_io.h"
9#include "xs_time.h"
10
11#include "snac.h"
12
13static xs_str *random_str(void)
14/* just what is says in the tin */
15{
16 unsigned int data[4] = {0};
17 FILE *f;
18
19 if ((f = fopen("/dev/random", "r")) != NULL) {
20 fread(data, sizeof(data), 1, f);
21 fclose(f);
22 }
23 else {
24 data[0] = random() % 0xffffffff;
25 data[1] = random() % 0xffffffff;
26 data[2] = random() % 0xffffffff;
27 data[3] = random() % 0xffffffff;
28 }
29
30 return xs_hex_enc((char *)data, sizeof(data));
31}
32
33
34int app_add(const char *id, const xs_dict *app)
35/* stores an app */
36{
37 if (!xs_is_hex(id))
38 return 500;
39
40 int status = 201;
41 xs *fn = xs_fmt("%s/app/", srv_basedir);
42 FILE *f;
43
44 mkdirx(fn);
45 fn = xs_str_cat(fn, id);
46 fn = xs_str_cat(fn, ".json");
47
48 if ((f = fopen(fn, "w")) != NULL) {
49 xs *j = xs_json_dumps_pp(app, 4);
50 fwrite(j, strlen(j), 1, f);
51 fclose(f);
52 }
53 else
54 status = 500;
55
56 return status;
57}
58
59
60xs_dict *app_get(const char *id)
61/* gets an app */
62{
63 if (!xs_is_hex(id))
64 return NULL;
65
66 xs *fn = xs_fmt("%s/app/%s.json", srv_basedir, id);
67 xs_dict *app = NULL;
68 FILE *f;
69
70 if ((f = fopen(fn, "r")) != NULL) {
71 xs *j = xs_readall(f);
72 fclose(f);
73
74 app = xs_json_loads(j);
75 }
76
77 return app;
78}
79
80
81int app_del(const char *id)
82/* deletes an app */
83{
84 if (!xs_is_hex(id))
85 return -1;
86
87 xs *fn = xs_fmt("%s/app/%s.json", srv_basedir, id);
88
89 return unlink(fn);
90}
91
92
93int token_add(const char *id, const xs_dict *token)
94/* stores a token */
95{
96 if (!xs_is_hex(id))
97 return 500;
98
99 int status = 201;
100 xs *fn = xs_fmt("%s/token/", srv_basedir);
101 FILE *f;
102
103 mkdirx(fn);
104 fn = xs_str_cat(fn, id);
105 fn = xs_str_cat(fn, ".json");
106
107 if ((f = fopen(fn, "w")) != NULL) {
108 xs *j = xs_json_dumps_pp(token, 4);
109 fwrite(j, strlen(j), 1, f);
110 fclose(f);
111 }
112 else
113 status = 500;
114
115 return status;
116}
117
118
119xs_dict *token_get(const char *id)
120/* gets a token */
121{
122 if (!xs_is_hex(id))
123 return NULL;
124
125 xs *fn = xs_fmt("%s/token/%s.json", srv_basedir, id);
126 xs_dict *token = NULL;
127 FILE *f;
128
129 if ((f = fopen(fn, "r")) != NULL) {
130 xs *j = xs_readall(f);
131 fclose(f);
132
133 token = xs_json_loads(j);
134 }
135
136 return token;
137}
138
139
140int token_del(const char *id)
141/* deletes a token */
142{
143 if (!xs_is_hex(id))
144 return -1;
145
146 xs *fn = xs_fmt("%s/token/%s.json", srv_basedir, id);
147
148 return unlink(fn);
149}
150
151
152const char *login_page = ""
153"<!DOCTYPE html>\n"
154"<body><h1>%s OAuth identify</h1>\n"
155"<div style=\"background-color: red; color: white\">%s</div>\n"
156"<form method=\"post\" action=\"https:/" "/%s/oauth/x-snac-login\">\n"
157"<p>Login: <input type=\"text\" name=\"login\"></p>\n"
158"<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
159"<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
160"<input type=\"hidden\" name=\"cid\" value=\"%s\">\n"
161"<input type=\"hidden\" name=\"state\" value=\"%s\">\n"
162"<input type=\"submit\" value=\"OK\">\n"
163"</form><p>%s</p></body>\n"
164"";
165
166int oauth_get_handler(const xs_dict *req, const char *q_path,
167 char **body, int *b_size, char **ctype)
168{
169 if (!xs_startswith(q_path, "/oauth/"))
170 return 0;
171
172 {
173 xs *j = xs_json_dumps_pp(req, 4);
174 printf("oauth get:\n%s\n", j);
175 }
176
177 int status = 404;
178 xs_dict *msg = xs_dict_get(req, "q_vars");
179 xs *cmd = xs_replace(q_path, "/oauth", "");
180
181 srv_debug(0, xs_fmt("oauth_get_handler %s", q_path));
182
183 if (strcmp(cmd, "/authorize") == 0) {
184 const char *cid = xs_dict_get(msg, "client_id");
185 const char *ruri = xs_dict_get(msg, "redirect_uri");
186 const char *rtype = xs_dict_get(msg, "response_type");
187 const char *state = xs_dict_get(msg, "state");
188
189 status = 400;
190
191 if (cid && ruri && rtype && strcmp(rtype, "code") == 0) {
192 xs *app = app_get(cid);
193
194 if (app != NULL) {
195 const char *host = xs_dict_get(srv_config, "host");
196
197 if (xs_is_null(state))
198 state = "";
199
200 *body = xs_fmt(login_page, host, "", host, ruri, cid, state, USER_AGENT);
201 *ctype = "text/html";
202 status = 200;
203
204 srv_debug(0, xs_fmt("oauth authorize: generating login page"));
205 }
206 else
207 srv_debug(0, xs_fmt("oauth authorize: bad client_id %s", cid));
208 }
209 else
210 srv_debug(0, xs_fmt("oauth authorize: invalid or unset arguments"));
211 }
212
213 return status;
214}
215
216
217int oauth_post_handler(const xs_dict *req, const char *q_path,
218 const char *payload, int p_size,
219 char **body, int *b_size, char **ctype)
220{
221 if (!xs_startswith(q_path, "/oauth/"))
222 return 0;
223
224 {
225 xs *j = xs_json_dumps_pp(req, 4);
226 printf("oauth post:\n%s\n", j);
227 }
228
229 int status = 404;
230 xs_dict *msg = xs_dict_get(req, "p_vars");
231 xs *cmd = xs_replace(q_path, "/oauth", "");
232
233 srv_debug(0, xs_fmt("oauth_post_handler %s", q_path));
234
235 if (strcmp(cmd, "/x-snac-login") == 0) {
236 const char *login = xs_dict_get(msg, "login");
237 const char *passwd = xs_dict_get(msg, "passwd");
238 const char *redir = xs_dict_get(msg, "redir");
239 const char *cid = xs_dict_get(msg, "cid");
240 const char *state = xs_dict_get(msg, "state");
241
242 const char *host = xs_dict_get(srv_config, "host");
243
244 /* by default, generate another login form with an error */
245 *body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, redir, cid, state, USER_AGENT);
246 *ctype = "text/html";
247 status = 200;
248
249 if (login && passwd && redir && cid) {
250 snac snac;
251
252 if (user_open(&snac, login)) {
253 /* check the login + password */
254 if (check_password(login, passwd,
255 xs_dict_get(snac.config, "passwd"))) {
256 /* success! redirect to the desired uri */
257 xs *code = random_str();
258
259 xs_free(*body);
260 *body = xs_fmt("%s?code=%s", redir, code);
261 status = 303;
262
263 /* if there is a state, add it */
264 if (!xs_is_null(state) && *state) {
265 *body = xs_str_cat(*body, "&state=");
266 *body = xs_str_cat(*body, state);
267 }
268
269 srv_debug(0, xs_fmt("oauth x-snac-login: success, redirect to %s", *body));
270
271 /* assign the login to the app */
272 xs *app = app_get(cid);
273
274 if (app != NULL) {
275 app = xs_dict_set(app, "uid", login);
276 app = xs_dict_set(app, "code", code);
277 app_add(cid, app);
278 }
279 else
280 srv_log(xs_fmt("oauth x-snac-login: error getting app %s", cid));
281 }
282 else
283 srv_debug(0, xs_fmt("oauth x-snac-login: login '%s' incorrect", login));
284
285 user_free(&snac);
286 }
287 else
288 srv_debug(0, xs_fmt("oauth x-snac-login: bad user '%s'", login));
289 }
290 else
291 srv_debug(0, xs_fmt("oauth x-snac-login: invalid or unset arguments"));
292 }
293 else
294 if (strcmp(cmd, "/token") == 0) {
295 const char *gtype = xs_dict_get(msg, "grant_type");
296 const char *code = xs_dict_get(msg, "code");
297 const char *cid = xs_dict_get(msg, "client_id");
298 const char *csec = xs_dict_get(msg, "client_secret");
299 const char *ruri = xs_dict_get(msg, "redirect_uri");
300 xs *wrk = NULL;
301
302 /* no client_secret? check if it's inside an authorization header
303 (AndStatus does it this way) */
304 if (xs_is_null(csec)) {
305 const char *auhdr = xs_dict_get(req, "authorization");
306
307 if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) {
308 xs *s1 = xs_replace(auhdr, "Basic ", "");
309 int size;
310 xs *s2 = xs_base64_dec(s1, &size);
311
312 if (!xs_is_null(s2)) {
313 xs *l1 = xs_split(s2, ":");
314
315 if (xs_list_len(l1) == 2) {
316 wrk = xs_dup(xs_list_get(l1, 1));
317 csec = wrk;
318 }
319 }
320 }
321 }
322
323 if (gtype && code && cid && csec && ruri) {
324 xs *app = app_get(cid);
325
326 if (app == NULL) {
327 status = 401;
328 srv_log(xs_fmt("oauth token: invalid app %s", cid));
329 }
330 else
331 if (strcmp(csec, xs_dict_get(app, "client_secret")) != 0) {
332 status = 401;
333 srv_log(xs_fmt("oauth token: invalid client_secret for app %s", cid));
334 }
335 else {
336 xs *rsp = xs_dict_new();
337 xs *cat = xs_number_new(time(NULL));
338 xs *tokid = random_str();
339
340 rsp = xs_dict_append(rsp, "access_token", tokid);
341 rsp = xs_dict_append(rsp, "token_type", "Bearer");
342 rsp = xs_dict_append(rsp, "created_at", cat);
343
344 *body = xs_json_dumps_pp(rsp, 4);
345 *ctype = "application/json";
346 status = 200;
347
348 const char *uid = xs_dict_get(app, "uid");
349
350 srv_debug(0, xs_fmt("oauth token: "
351 "successful login for %s, new token %s", uid, tokid));
352
353 xs *token = xs_dict_new();
354 token = xs_dict_append(token, "token", tokid);
355 token = xs_dict_append(token, "client_id", cid);
356 token = xs_dict_append(token, "client_secret", csec);
357 token = xs_dict_append(token, "uid", uid);
358 token = xs_dict_append(token, "code", code);
359
360 token_add(tokid, token);
361 }
362 }
363 else {
364 srv_debug(0, xs_fmt("oauth token: invalid or unset arguments"));
365 status = 400;
366 }
367 }
368 else
369 if (strcmp(cmd, "/revoke") == 0) {
370 const char *cid = xs_dict_get(msg, "client_id");
371 const char *csec = xs_dict_get(msg, "client_secret");
372 const char *tokid = xs_dict_get(msg, "token");
373
374 if (cid && csec && tokid) {
375 xs *token = token_get(tokid);
376
377 *body = xs_str_new("{}");
378 *ctype = "application/json";
379
380 if (token == NULL || strcmp(csec, xs_dict_get(token, "client_secret")) != 0) {
381 srv_debug(0, xs_fmt("oauth revoke: bad secret for token %s", tokid));
382 status = 403;
383 }
384 else {
385 token_del(tokid);
386 srv_debug(0, xs_fmt("oauth revoke: revoked token %s", tokid));
387 status = 200;
388
389 /* also delete the app, as it serves no purpose from now on */
390 app_del(cid);
391 }
392 }
393 else {
394 srv_debug(0, xs_fmt("oauth revoke: invalid or unset arguments"));
395 status = 403;
396 }
397 }
398
399 return status;
400}
401
402
403xs_str *mastoapi_id(const xs_dict *msg)
404/* returns a somewhat Mastodon-compatible status id */
405{
406 char tmp[256] = "";
407 int n = 0;
408 const char *id = xs_dict_get(msg, "id");
409 const char *published = xs_dict_get(msg, "published");
410
411 if (!xs_is_null(published)) {
412 /* transfer all numbers from the published date */
413 while (*published && n < sizeof(tmp) - 1) {
414 if (*published >= '0' && *published <= '9')
415 tmp[n++] = *published;
416 published++;
417 }
418 tmp[n] = '\0';
419 }
420
421 xs *md5 = xs_md5_hex(id, strlen(id));
422
423 return xs_str_cat(xs_str_new(tmp), md5);
424}
425
426
427int mastoapi_get_handler(const xs_dict *req, const char *q_path,
428 char **body, int *b_size, char **ctype)
429{
430 if (!xs_startswith(q_path, "/api/v1/"))
431 return 0;
432
433 srv_debug(0, xs_fmt("mastoapi_get_handler %s", q_path));
434 {
435 xs *j = xs_json_dumps_pp(req, 4);
436 printf("mastoapi get:\n%s\n", j);
437 }
438
439 int status = 404;
440 xs_dict *args = xs_dict_get(req, "q_vars");
441 xs *cmd = xs_replace(q_path, "/api/v1", "");
442 char *v;
443
444 snac snac = {0};
445 int logged_in = 0;
446
447 /* if there is an authorization field, try to validate it */
448 if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
449 xs *tokid = xs_replace(v, "Bearer ", "");
450 xs *token = token_get(tokid);
451
452 if (token != NULL) {
453 const char *uid = xs_dict_get(token, "uid");
454
455 if (!xs_is_null(uid) && user_open(&snac, uid)) {
456 logged_in = 1;
457 srv_debug(0, xs_fmt("mastoapi auth: valid token for user %s", uid));
458 }
459 else
460 srv_log(xs_fmt("mastoapi auth: corrupted token %s", tokid));
461 }
462 else
463 srv_log(xs_fmt("mastoapi auth: invalid token %s", tokid));
464 }
465
466 if (strcmp(cmd, "/accounts/verify_credentials") == 0) {
467 if (logged_in) {
468 xs *acct = xs_dict_new();
469
470 acct = xs_dict_append(acct, "id", xs_dict_get(snac.config, "uid"));
471 acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
472 acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
473 acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name"));
474 acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published"));
475 acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
476 acct = xs_dict_append(acct, "url", snac.actor);
477
478 xs *avatar = NULL;
479 char *av = xs_dict_get(snac.config, "avatar");
480
481 if (xs_is_null(av) || *av == '\0')
482 avatar = xs_fmt("%s/susie.png", srv_baseurl);
483 else
484 avatar = xs_dup(av);
485
486 acct = xs_dict_append(acct, "avatar", avatar);
487
488 *body = xs_json_dumps_pp(acct, 4);
489 *ctype = "application/json";
490 status = 200;
491 }
492 else {
493 status = 422; // "Unprocessable entity" (no login)
494 }
495 }
496 else
497 if (strcmp(cmd, "/timelines/home") == 0) {
498 /* the private timeline */
499 if (logged_in) {
500 const char *max_id = xs_dict_get(args, "max_id");
501 const char *since_id = xs_dict_get(args, "since_id");
502 const char *min_id = xs_dict_get(args, "min_id");
503 const char *limit_s = xs_dict_get(args, "limit");
504 int limit = 0;
505 int cnt = 0;
506
507 if (!xs_is_null(limit_s))
508 limit = atoi(limit_s);
509
510 if (limit == 0)
511 limit = 20;
512
513 xs *timeline = timeline_simple_list(&snac, "private", 0, XS_ALL);
514
515 xs *out = xs_list_new();
516 xs_list *p = timeline;
517 xs_str *v;
518
519 while (xs_list_iter(&p, &v) && cnt < limit) {
520 xs *msg = NULL;
521
522 /* only return entries older that max_id */
523 if (max_id) {
524 if (strcmp(v, max_id) == 0)
525 max_id = NULL;
526
527 continue;
528 }
529
530 /* only returns entries newer than since_id */
531 if (since_id) {
532 if (strcmp(v, since_id) == 0)
533 break;
534 }
535
536 /* only returns entries newer than min_id */
537 /* what does really "Return results immediately newer than ID" mean? */
538 if (min_id) {
539 if (strcmp(v, min_id) == 0)
540 break;
541 }
542
543 /* get the entry */
544 if (!valid_status(timeline_get_by_md5(&snac, v, &msg)))
545 continue;
546
547 /* discard non-Notes */
548 if (strcmp(xs_dict_get(msg, "type"), "Note") != 0)
549 continue;
550
551 xs *actor = NULL;
552 actor_get(&snac, xs_dict_get(msg, "attributedTo"), &actor);
553
554 /* if the author is not here, discard */
555 if (actor == NULL)
556 continue;
557
558 /** shave the yak converting an ActivityPub Note to a Mastodon status **/
559
560 xs *acct = xs_dict_new();
561
562 const char *display_name = xs_dict_get(actor, "name");
563 if (xs_is_null(display_name) || *display_name == '\0')
564 display_name = xs_dict_get(actor, "preferredUsername");
565
566 const char *id = xs_dict_get(actor, "id");
567 const char *pub = xs_dict_get(actor, "published");
568 xs *acct_md5 = xs_md5_hex(id, strlen(id));
569 acct = xs_dict_append(acct, "id", acct_md5);
570 acct = xs_dict_append(acct, "username", xs_dict_get(actor, "preferredUsername"));
571 acct = xs_dict_append(acct, "acct", xs_dict_get(actor, "preferredUsername"));
572 acct = xs_dict_append(acct, "display_name", display_name);
573
574 if (pub)
575 acct = xs_dict_append(acct, "created_at", pub);
576
577 acct = xs_dict_append(acct, "note", xs_dict_get(actor, "summary"));
578 acct = xs_dict_append(acct, "url", id);
579
580 xs *avatar = NULL;
581 xs_dict *av = xs_dict_get(actor, "icon");
582
583 if (xs_type(av) == XSTYPE_DICT)
584 avatar = xs_dup(xs_dict_get(av, "url"));
585 else
586 avatar = xs_fmt("%s/susie.png", srv_baseurl);
587
588 acct = xs_dict_append(acct, "avatar", avatar);
589
590 xs *f = xs_val_new(XSTYPE_FALSE);
591 xs *t = xs_val_new(XSTYPE_TRUE);
592 xs *n = xs_val_new(XSTYPE_NULL);
593 xs *el = xs_list_new();
594 xs *idx = NULL;
595 xs *ixc = NULL;
596
597 char *tmp;
598 id = xs_dict_get(msg, "id");
599 xs *mid = mastoapi_id(msg);
600
601 xs *st = xs_dict_new();
602
603 st = xs_dict_append(st, "id", mid);
604 st = xs_dict_append(st, "uri", id);
605 st = xs_dict_append(st, "url", id);
606 st = xs_dict_append(st, "created_at", xs_dict_get(msg, "published"));
607 st = xs_dict_append(st, "account", acct);
608 st = xs_dict_append(st, "content", xs_dict_get(msg, "content"));
609
610 st = xs_dict_append(st, "visibility",
611 is_msg_public(&snac, msg) ? "public" : "private");
612
613 tmp = xs_dict_get(msg, "sensitive");
614 if (xs_is_null(tmp))
615 tmp = f;
616
617 st = xs_dict_append(st, "sensitive", tmp);
618
619 tmp = xs_dict_get(msg, "summary");
620 if (xs_is_null(tmp))
621 tmp = "";
622
623 st = xs_dict_append(st, "spoiler_text", tmp);
624
625 /* create the list of attachments */
626 xs *matt = xs_list_new();
627 xs_list *att = xs_dict_get(msg, "attachment");
628 xs_str *aobj;
629
630 while (xs_list_iter(&att, &aobj)) {
631 const char *mtype = xs_dict_get(aobj, "mediaType");
632
633 if (!xs_is_null(mtype) && xs_startswith(mtype, "image/")) {
634 xs *matteid = xs_fmt("%s_%d", id, xs_list_len(matt));
635 xs *matte = xs_dict_new();
636
637 matte = xs_dict_append(matte, "id", matteid);
638 matte = xs_dict_append(matte, "type", "image");
639 matte = xs_dict_append(matte, "url", xs_dict_get(aobj, "url"));
640 matte = xs_dict_append(matte, "preview_url", xs_dict_get(aobj, "url"));
641 matte = xs_dict_append(matte, "remote_url", xs_dict_get(aobj, "url"));
642 matte = xs_dict_append(matte, "description", xs_dict_get(aobj, "name"));
643
644 matt = xs_list_append(matt, matte);
645 }
646 }
647
648 st = xs_dict_append(st, "media_attachments", matt);
649
650 st = xs_dict_append(st, "mentions", el);
651 st = xs_dict_append(st, "tags", el);
652 st = xs_dict_append(st, "emojis", el);
653
654 xs_free(idx);
655 xs_free(ixc);
656 idx = object_likes(id);
657 ixc = xs_number_new(xs_list_len(idx));
658
659 st = xs_dict_append(st, "favourites_count", ixc);
660 st = xs_dict_append(st, "favourited",
661 xs_list_in(idx, snac.md5) != -1 ? t : f);
662
663 xs_free(idx);
664 xs_free(ixc);
665 idx = object_announces(id);
666 ixc = xs_number_new(xs_list_len(idx));
667
668 st = xs_dict_append(st, "reblogs_count", ixc);
669 st = xs_dict_append(st, "reblogged",
670 xs_list_in(idx, snac.md5) != -1 ? t : f);
671
672 xs_free(idx);
673 xs_free(ixc);
674 idx = object_children(id);
675 ixc = xs_number_new(xs_list_len(idx));
676
677 st = xs_dict_append(st, "replies_count", ixc);
678
679 /* default in_reply_to values */
680 st = xs_dict_append(st, "in_reply_to_id", n);
681 st = xs_dict_append(st, "in_reply_to_account_id", n);
682
683 tmp = xs_dict_get(msg, "inReplyTo");
684 if (!xs_is_null(tmp)) {
685 xs *irto = NULL;
686
687 if (valid_status(object_get(tmp, &irto))) {
688 xs *irt_mid = mastoapi_id(irto);
689 st = xs_dict_set(st, "in_reply_to_id", irt_mid);
690
691 char *at = NULL;
692 if (!xs_is_null(at = xs_dict_get(irto, "attributedTo"))) {
693 xs *at_md5 = xs_md5_hex(at, strlen(at));
694 st = xs_dict_set(st, "in_reply_to_account_id", at_md5);
695 }
696 }
697 }
698
699 st = xs_dict_append(st, "reblog", n);
700 st = xs_dict_append(st, "poll", n);
701 st = xs_dict_append(st, "card", n);
702 st = xs_dict_append(st, "language", n);
703
704 tmp = xs_dict_get(msg, "sourceContent");
705 if (xs_is_null(tmp))
706 tmp = "";
707
708 st = xs_dict_append(st, "text", tmp);
709
710 tmp = xs_dict_get(msg, "updated");
711 if (xs_is_null(tmp))
712 tmp = n;
713
714 st = xs_dict_append(st, "edited_at", tmp);
715
716 out = xs_list_append(out, st);
717
718 cnt++;
719 }
720
721 *body = xs_json_dumps_pp(out, 4);
722 *ctype = "application/json";
723 status = 200;
724
725 srv_debug(0, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out)));
726 }
727 else {
728 status = 401; // unauthorized
729 }
730 }
731 else
732 if (strcmp(cmd, "/timelines/public") == 0) {
733 /* the public timeline */
734 }
735
736 /* user cleanup */
737 if (logged_in)
738 user_free(&snac);
739
740 return status;
741}
742
743
744int mastoapi_post_handler(const xs_dict *req, const char *q_path,
745 const char *payload, int p_size,
746 char **body, int *b_size, char **ctype)
747{
748 if (!xs_startswith(q_path, "/api/v1/"))
749 return 0;
750
751 srv_debug(0, xs_fmt("mastoapi_post_handler %s", q_path));
752 {
753 xs *j = xs_json_dumps_pp(req, 4);
754 printf("mastoapi post:\n%s\n", j);
755 }
756
757 int status = 404;
758 xs *msg = NULL;
759 char *i_ctype = xs_dict_get(req, "content-type");
760
761 if (xs_startswith(i_ctype, "application/json"))
762 msg = xs_json_loads(payload);
763 else
764 msg = xs_dup(xs_dict_get(req, "p_vars"));
765
766 if (msg == NULL)
767 return 400;
768
769 {
770 xs *j = xs_json_dumps_pp(msg, 4);
771 printf("%s\n", j);
772 }
773
774 xs *cmd = xs_replace(q_path, "/api/v1", "");
775
776 if (strcmp(cmd, "/apps") == 0) {
777 const char *name = xs_dict_get(msg, "client_name");
778 const char *ruri = xs_dict_get(msg, "redirect_uris");
779 const char *scope = xs_dict_get(msg, "scope");
780
781 if (xs_type(ruri) == XSTYPE_LIST)
782 ruri = xs_dict_get(ruri, 0);
783
784 if (name && ruri) {
785 xs *app = xs_dict_new();
786 xs *id = xs_replace_i(tid(0), ".", "");
787 xs *cid = random_str();
788 xs *csec = random_str();
789 xs *vkey = random_str();
790
791 app = xs_dict_append(app, "name", name);
792 app = xs_dict_append(app, "redirect_uri", ruri);
793 app = xs_dict_append(app, "client_id", cid);
794 app = xs_dict_append(app, "client_secret", csec);
795 app = xs_dict_append(app, "vapid_key", vkey);
796 app = xs_dict_append(app, "id", id);
797
798 *body = xs_json_dumps_pp(app, 4);
799 *ctype = "application/json";
800 status = 200;
801
802 app = xs_dict_append(app, "code", "");
803
804 if (scope)
805 app = xs_dict_append(app, "scope", scope);
806
807 app_add(cid, app);
808
809 srv_debug(0, xs_fmt("mastoapi apps: new app %s", cid));
810 }
811 }
812
813 return status;
814}
diff --git a/snac.h b/snac.h
index 6477040..f33b806 100644
--- a/snac.h
+++ b/snac.h
@@ -223,3 +223,14 @@ int resetpwd(snac *snac);
223int job_fifo_ready(void); 223int job_fifo_ready(void);
224void job_post(const xs_val *job, int urgent); 224void job_post(const xs_val *job, int urgent);
225void job_wait(xs_val **job); 225void job_wait(xs_val **job);
226
227int oauth_get_handler(const xs_dict *req, const char *q_path,
228 char **body, int *b_size, char **ctype);
229int oauth_post_handler(const xs_dict *req, const char *q_path,
230 const char *payload, int p_size,
231 char **body, int *b_size, char **ctype);
232int mastoapi_get_handler(const xs_dict *req, const char *q_path,
233 char **body, int *b_size, char **ctype);
234int mastoapi_post_handler(const xs_dict *req, const char *q_path,
235 const char *payload, int p_size,
236 char **body, int *b_size, char **ctype);
diff --git a/xs_encdec.h b/xs_encdec.h
index 5966583..b88736e 100644
--- a/xs_encdec.h
+++ b/xs_encdec.h
@@ -6,6 +6,7 @@
6 6
7 xs_str *xs_hex_enc(const xs_val *data, int size); 7 xs_str *xs_hex_enc(const xs_val *data, int size);
8 xs_val *xs_hex_dec(const xs_str *hex, int *size); 8 xs_val *xs_hex_dec(const xs_str *hex, int *size);
9 int xs_is_hex(const char *str);
9 xs_str *xs_base64_enc(const xs_val *data, int sz); 10 xs_str *xs_base64_enc(const xs_val *data, int sz);
10 xs_val *xs_base64_dec(const xs_str *data, int *size); 11 xs_val *xs_base64_dec(const xs_str *data, int *size);
11 xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint); 12 xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint);
@@ -65,6 +66,18 @@ xs_val *xs_hex_dec(const xs_str *hex, int *size)
65} 66}
66 67
67 68
69int xs_is_hex(const char *str)
70/* returns 1 if str is an hex string */
71{
72 while (*str) {
73 if (strchr("0123456789abcdefABCDEF", *str++) == NULL)
74 return 0;
75 }
76
77 return 1;
78}
79
80
68xs_str *xs_base64_enc(const xs_val *data, int sz) 81xs_str *xs_base64_enc(const xs_val *data, int sz)
69/* encodes data to base64 */ 82/* encodes data to base64 */
70{ 83{
diff --git a/xs_version.h b/xs_version.h
index 559fab6..eff4ddf 100644
--- a/xs_version.h
+++ b/xs_version.h
@@ -1 +1 @@
/* b4afa5f823a998a263159ebfe9be67b81a8cc774 */ /* 69d6e64d31491688ba4411e71c55e6c25482b17e */