diff options
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | RELEASE_NOTES.md | 10 | ||||
| -rw-r--r-- | activitypub.c | 56 | ||||
| -rw-r--r-- | data.c | 121 | ||||
| -rw-r--r-- | doc/snac.1 | 30 | ||||
| -rwxr-xr-x | examples/docker-entrypoint.sh | 2 | ||||
| -rw-r--r-- | html.c | 27 | ||||
| -rw-r--r-- | http.c | 2 | ||||
| -rw-r--r-- | httpd.c | 8 | ||||
| -rw-r--r-- | main.c | 22 | ||||
| -rw-r--r-- | mastoapi.c | 201 | ||||
| -rw-r--r-- | snac.h | 31 | ||||
| -rw-r--r-- | webfinger.c | 8 | ||||
| -rw-r--r-- | xs.h | 12 | ||||
| -rw-r--r-- | xs_encdec.h | 221 | ||||
| -rw-r--r-- | xs_version.h | 2 |
16 files changed, 610 insertions, 147 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | PREFIX=/usr/local | 1 | PREFIX=/usr/local |
| 2 | PREFIX_MAN=$(PREFIX)/man | 2 | PREFIX_MAN=$(PREFIX)/man |
| 3 | CFLAGS?=-g -Wall | 3 | CFLAGS?=-g -Wall -Wextra |
| 4 | 4 | ||
| 5 | all: snac | 5 | all: snac |
| 6 | 6 | ||
| @@ -40,7 +40,7 @@ httpd.o: httpd.c xs.h xs_io.h xs_encdec.h xs_json.h xs_socket.h \ | |||
| 40 | xs_httpd.h xs_mime.h snac.h | 40 | xs_httpd.h xs_mime.h snac.h |
| 41 | main.o: main.c xs.h xs_io.h xs_encdec.h xs_json.h snac.h | 41 | main.o: main.c xs.h xs_io.h xs_encdec.h xs_json.h snac.h |
| 42 | mastoapi.o: mastoapi.c xs.h xs_encdec.h xs_openssl.h xs_json.h xs_io.h \ | 42 | mastoapi.o: mastoapi.c xs.h xs_encdec.h xs_openssl.h xs_json.h xs_io.h \ |
| 43 | xs_time.h snac.h | 43 | xs_time.h xs_glob.h snac.h |
| 44 | snac.o: snac.c xs.h xs_io.h xs_encdec.h xs_json.h xs_curl.h xs_openssl.h \ | 44 | snac.o: snac.c xs.h xs_io.h xs_encdec.h xs_json.h xs_curl.h xs_openssl.h \ |
| 45 | xs_socket.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h \ | 45 | xs_socket.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h \ |
| 46 | snac.h | 46 | snac.h |
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8a153e9..1af4b73 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md | |||
| @@ -1,5 +1,15 @@ | |||
| 1 | # Release Notes | 1 | # Release Notes |
| 2 | 2 | ||
| 3 | ## 2.30 | ||
| 4 | |||
| 5 | Fixed a bug that made some notifications to be missed. | ||
| 6 | |||
| 7 | New Mastodon API features: the instance public timeline is now a real one, unfavourite / unreblog is supported (somewhat). Some regression bugs regarding image posting were also fixed. | ||
| 8 | |||
| 9 | The non-standard `Ping` and `Pong` #ActivityPub activities have been implemented as proposed by @tedu@honk.tedunangst.com in the https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt document (with a minor diversion: retries are managed in the same way as the rest of #snac messages). | ||
| 10 | |||
| 11 | The build process now includes the `-Wextra` flag. | ||
| 12 | |||
| 3 | ## 2.29 | 13 | ## 2.29 |
| 4 | 14 | ||
| 5 | New Mastodon API features: account search, relationships (so the Follow/Unfollow buttons now appear for each account), follow and unfollow accounts, an instance-level timeline (very kludgy), custom emojis for accounts and statuses, many bug fixes (sadly, the Mastodon official app still does not work). | 15 | New Mastodon API features: account search, relationships (so the Follow/Unfollow buttons now appear for each account), follow and unfollow accounts, an instance-level timeline (very kludgy), custom emojis for accounts and statuses, many bug fixes (sadly, the Mastodon official app still does not work). |
diff --git a/activitypub.c b/activitypub.c index c8c166d..9069d03 100644 --- a/activitypub.c +++ b/activitypub.c | |||
| @@ -109,7 +109,7 @@ int actor_request(snac *snac, const char *actor, xs_dict **data) | |||
| 109 | 109 | ||
| 110 | if (valid_status(status2)) { | 110 | if (valid_status(status2)) { |
| 111 | /* renew data */ | 111 | /* renew data */ |
| 112 | status = actor_add(snac, actor, payload); | 112 | status = actor_add(actor, payload); |
| 113 | 113 | ||
| 114 | if (data != NULL) { | 114 | if (data != NULL) { |
| 115 | *data = payload; | 115 | *data = payload; |
| @@ -437,7 +437,8 @@ void process_tags(snac *snac, const char *content, d_char **n_content, d_char ** | |||
| 437 | 437 | ||
| 438 | /** messages **/ | 438 | /** messages **/ |
| 439 | 439 | ||
| 440 | d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char *object) | 440 | xs_dict *msg_base(snac *snac, const char *type, const char *id, |
| 441 | const char *actor, const char *date, const char *object) | ||
| 441 | /* creates a base ActivityPub message */ | 442 | /* creates a base ActivityPub message */ |
| 442 | { | 443 | { |
| 443 | xs *did = NULL; | 444 | xs *did = NULL; |
| @@ -467,7 +468,7 @@ d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char | |||
| 467 | } | 468 | } |
| 468 | } | 469 | } |
| 469 | 470 | ||
| 470 | d_char *msg = xs_dict_new(); | 471 | xs_dict *msg = xs_dict_new(); |
| 471 | 472 | ||
| 472 | msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams"); | 473 | msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams"); |
| 473 | msg = xs_dict_append(msg, "type", type); | 474 | msg = xs_dict_append(msg, "type", type); |
| @@ -845,6 +846,28 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, | |||
| 845 | } | 846 | } |
| 846 | 847 | ||
| 847 | 848 | ||
| 849 | xs_dict *msg_ping(snac *user, const char *rcpt) | ||
| 850 | /* creates a Ping message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */ | ||
| 851 | { | ||
| 852 | xs_dict *msg = msg_base(user, "Ping", "@dummy", user->actor, NULL, NULL); | ||
| 853 | |||
| 854 | msg = xs_dict_append(msg, "to", rcpt); | ||
| 855 | |||
| 856 | return msg; | ||
| 857 | } | ||
| 858 | |||
| 859 | |||
| 860 | xs_dict *msg_pong(snac *user, const char *rcpt, const char *object) | ||
| 861 | /* creates a Pong message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */ | ||
| 862 | { | ||
| 863 | xs_dict *msg = msg_base(user, "Pong", "@dummy", user->actor, NULL, object); | ||
| 864 | |||
| 865 | msg = xs_dict_append(msg, "to", rcpt); | ||
| 866 | |||
| 867 | return msg; | ||
| 868 | } | ||
| 869 | |||
| 870 | |||
| 848 | void notify(snac *snac, xs_str *type, xs_str *utype, xs_str *actor, xs_dict *msg) | 871 | void notify(snac *snac, xs_str *type, xs_str *utype, xs_str *actor, xs_dict *msg) |
| 849 | /* notifies the user of relevant events */ | 872 | /* notifies the user of relevant events */ |
| 850 | { | 873 | { |
| @@ -1121,7 +1144,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) | |||
| 1121 | else | 1144 | else |
| 1122 | if (strcmp(type, "Update") == 0) { | 1145 | if (strcmp(type, "Update") == 0) { |
| 1123 | if (strcmp(utype, "Person") == 0) { | 1146 | if (strcmp(utype, "Person") == 0) { |
| 1124 | actor_add(snac, actor, xs_dict_get(msg, "object")); | 1147 | actor_add(actor, xs_dict_get(msg, "object")); |
| 1125 | 1148 | ||
| 1126 | snac_log(snac, xs_fmt("updated actor %s", actor)); | 1149 | snac_log(snac, xs_fmt("updated actor %s", actor)); |
| 1127 | } | 1150 | } |
| @@ -1147,7 +1170,19 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) | |||
| 1147 | snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object)); | 1170 | snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object)); |
| 1148 | } | 1171 | } |
| 1149 | else | 1172 | else |
| 1150 | snac_debug(snac, 1, xs_fmt("process_message type '%s' ignored", type)); | 1173 | if (strcmp(type, "Pong") == 0) { |
| 1174 | snac_log(snac, xs_fmt("'Pong' received from %s", actor)); | ||
| 1175 | } | ||
| 1176 | else | ||
| 1177 | if (strcmp(type, "Ping") == 0) { | ||
| 1178 | snac_log(snac, xs_fmt("'Ping' requested from %s", actor)); | ||
| 1179 | |||
| 1180 | xs *rsp = msg_pong(snac, actor, xs_dict_get(msg, "id")); | ||
| 1181 | |||
| 1182 | enqueue_output_by_actor(snac, rsp, actor, 0); | ||
| 1183 | } | ||
| 1184 | else | ||
| 1185 | snac_debug(snac, 1, xs_fmt("process_input_message type '%s' ignored", type)); | ||
| 1151 | 1186 | ||
| 1152 | if (do_notify) { | 1187 | if (do_notify) { |
| 1153 | notify(snac, type, utype, actor, msg); | 1188 | notify(snac, type, utype, actor, msg); |
| @@ -1384,8 +1419,9 @@ void process_queue_item(xs_dict *q_item) | |||
| 1384 | xs *headers = xs_dict_new(); | 1419 | xs *headers = xs_dict_new(); |
| 1385 | headers = xs_dict_append(headers, "content-type", "application/json"); | 1420 | headers = xs_dict_append(headers, "content-type", "application/json"); |
| 1386 | 1421 | ||
| 1387 | xs *rsp = xs_http_request("POST", url, headers, | 1422 | xs *rsp = xs_http_request("POST", url, headers, |
| 1388 | body, strlen(body), &status, NULL, NULL, 0); | 1423 | body, strlen(body), &status, NULL, NULL, 0); |
| 1424 | rsp = xs_free(rsp); | ||
| 1389 | 1425 | ||
| 1390 | srv_debug(0, xs_fmt("telegram post %d", status)); | 1426 | srv_debug(0, xs_fmt("telegram post %d", status)); |
| 1391 | } | 1427 | } |
| @@ -1426,7 +1462,7 @@ int process_queue(void) | |||
| 1426 | 1462 | ||
| 1427 | /** HTTP handlers */ | 1463 | /** HTTP handlers */ |
| 1428 | 1464 | ||
| 1429 | int activitypub_get_handler(d_char *req, char *q_path, | 1465 | int activitypub_get_handler(const xs_dict *req, const char *q_path, |
| 1430 | char **body, int *b_size, char **ctype) | 1466 | char **body, int *b_size, char **ctype) |
| 1431 | { | 1467 | { |
| 1432 | int status = 200; | 1468 | int status = 200; |
| @@ -1519,11 +1555,13 @@ int activitypub_get_handler(d_char *req, char *q_path, | |||
| 1519 | } | 1555 | } |
| 1520 | 1556 | ||
| 1521 | 1557 | ||
| 1522 | int activitypub_post_handler(d_char *req, char *q_path, | 1558 | int activitypub_post_handler(const xs_dict *req, const char *q_path, |
| 1523 | d_char *payload, int p_size, | 1559 | char *payload, int p_size, |
| 1524 | char **body, int *b_size, char **ctype) | 1560 | char **body, int *b_size, char **ctype) |
| 1525 | /* processes an input message */ | 1561 | /* processes an input message */ |
| 1526 | { | 1562 | { |
| 1563 | (void)b_size; | ||
| 1564 | |||
| 1527 | int status = 202; /* accepted */ | 1565 | int status = 202; /* accepted */ |
| 1528 | char *i_ctype = xs_dict_get(req, "content-type"); | 1566 | char *i_ctype = xs_dict_get(req, "content-type"); |
| 1529 | snac snac; | 1567 | snac snac; |
| @@ -328,6 +328,51 @@ int index_add(const char *fn, const char *id) | |||
| 328 | } | 328 | } |
| 329 | 329 | ||
| 330 | 330 | ||
| 331 | int index_del_md5(const char *fn, const char *md5) | ||
| 332 | /* deletes an md5 from an index */ | ||
| 333 | { | ||
| 334 | int status = 404; | ||
| 335 | FILE *f; | ||
| 336 | |||
| 337 | pthread_mutex_lock(&data_mutex); | ||
| 338 | |||
| 339 | if ((f = fopen(fn, "r+")) != NULL) { | ||
| 340 | char line[256]; | ||
| 341 | |||
| 342 | while (fgets(line, sizeof(line), f) != NULL) { | ||
| 343 | line[32] = '\0'; | ||
| 344 | |||
| 345 | if (strcmp(line, md5) == 0) { | ||
| 346 | /* found! just rewind, overwrite it with garbage | ||
| 347 | and an eventual call to index_gc() will clean it | ||
| 348 | [yes: this breaks index_len()] */ | ||
| 349 | fseek(f, -33, SEEK_CUR); | ||
| 350 | fwrite("-", 1, 1, f); | ||
| 351 | status = 200; | ||
| 352 | |||
| 353 | break; | ||
| 354 | } | ||
| 355 | } | ||
| 356 | |||
| 357 | fclose(f); | ||
| 358 | } | ||
| 359 | else | ||
| 360 | status = 500; | ||
| 361 | |||
| 362 | pthread_mutex_unlock(&data_mutex); | ||
| 363 | |||
| 364 | return status; | ||
| 365 | } | ||
| 366 | |||
| 367 | |||
| 368 | int index_del(const char *fn, const char *id) | ||
| 369 | /* deletes an id from an index */ | ||
| 370 | { | ||
| 371 | xs *md5 = xs_md5_hex(id, strlen(id)); | ||
| 372 | return index_del_md5(fn, md5); | ||
| 373 | } | ||
| 374 | |||
| 375 | |||
| 331 | int index_gc(const char *fn) | 376 | int index_gc(const char *fn) |
| 332 | /* garbage-collects an index, deleting objects that are not here */ | 377 | /* garbage-collects an index, deleting objects that are not here */ |
| 333 | { | 378 | { |
| @@ -772,6 +817,23 @@ int object_admire(const char *id, const char *actor, int like) | |||
| 772 | } | 817 | } |
| 773 | 818 | ||
| 774 | 819 | ||
| 820 | int object_unadmire(const char *id, const char *actor, int like) | ||
| 821 | /* actor no longer likes or announces this object */ | ||
| 822 | { | ||
| 823 | int status; | ||
| 824 | xs *fn = _object_fn(id); | ||
| 825 | |||
| 826 | fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx"); | ||
| 827 | |||
| 828 | status = index_del(fn, actor); | ||
| 829 | |||
| 830 | srv_debug(0, | ||
| 831 | xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status)); | ||
| 832 | |||
| 833 | return status; | ||
| 834 | } | ||
| 835 | |||
| 836 | |||
| 775 | int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del) | 837 | int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del) |
| 776 | /* adds or deletes from a user cache */ | 838 | /* adds or deletes from a user cache */ |
| 777 | { | 839 | { |
| @@ -969,8 +1031,13 @@ void timeline_update_indexes(snac *snac, const char *id) | |||
| 969 | 1031 | ||
| 970 | if (valid_status(object_get(id, &msg))) { | 1032 | if (valid_status(object_get(id, &msg))) { |
| 971 | /* if its ours and is public, also store in public */ | 1033 | /* if its ours and is public, also store in public */ |
| 972 | if (is_msg_public(snac, msg)) | 1034 | if (is_msg_public(snac, msg)) { |
| 973 | object_user_cache_add(snac, id, "public"); | 1035 | object_user_cache_add(snac, id, "public"); |
| 1036 | |||
| 1037 | /* also add it to the instance public timeline */ | ||
| 1038 | xs *ipt = xs_fmt("%s/public.idx", srv_basedir); | ||
| 1039 | index_add(ipt, id); | ||
| 1040 | } | ||
| 974 | } | 1041 | } |
| 975 | } | 1042 | } |
| 976 | } | 1043 | } |
| @@ -1041,7 +1108,7 @@ xs_list *timeline_top_level(snac *snac, xs_list *list) | |||
| 1041 | } | 1108 | } |
| 1042 | 1109 | ||
| 1043 | 1110 | ||
| 1044 | d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show) | 1111 | xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show) |
| 1045 | /* returns a timeline (with all entries) */ | 1112 | /* returns a timeline (with all entries) */ |
| 1046 | { | 1113 | { |
| 1047 | int c_max; | 1114 | int c_max; |
| @@ -1059,7 +1126,7 @@ d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int sho | |||
| 1059 | } | 1126 | } |
| 1060 | 1127 | ||
| 1061 | 1128 | ||
| 1062 | d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show) | 1129 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) |
| 1063 | /* returns a timeline (only top level entries) */ | 1130 | /* returns a timeline (only top level entries) */ |
| 1064 | { | 1131 | { |
| 1065 | xs *list = timeline_simple_list(snac, idx_name, skip, show); | 1132 | xs *list = timeline_simple_list(snac, idx_name, skip, show); |
| @@ -1068,6 +1135,15 @@ d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show) | |||
| 1068 | } | 1135 | } |
| 1069 | 1136 | ||
| 1070 | 1137 | ||
| 1138 | xs_list *timeline_instance_list(int skip, int show) | ||
| 1139 | /* returns the timeline for the full instance */ | ||
| 1140 | { | ||
| 1141 | xs *idx = xs_fmt("%s/public.idx", srv_basedir); | ||
| 1142 | |||
| 1143 | return index_list_desc(idx, skip, show); | ||
| 1144 | } | ||
| 1145 | |||
| 1146 | |||
| 1071 | /** following **/ | 1147 | /** following **/ |
| 1072 | 1148 | ||
| 1073 | /* this needs special treatment and cannot use the object db as is, | 1149 | /* this needs special treatment and cannot use the object db as is, |
| @@ -1273,30 +1349,49 @@ int is_hidden(snac *snac, const char *id) | |||
| 1273 | } | 1349 | } |
| 1274 | 1350 | ||
| 1275 | 1351 | ||
| 1276 | int actor_add(snac *snac, const char *actor, d_char *msg) | 1352 | int actor_add(const char *actor, xs_dict *msg) |
| 1277 | /* adds an actor */ | 1353 | /* adds an actor */ |
| 1278 | { | 1354 | { |
| 1279 | return object_add_ow(actor, msg); | 1355 | return object_add_ow(actor, msg); |
| 1280 | } | 1356 | } |
| 1281 | 1357 | ||
| 1282 | 1358 | ||
| 1283 | int actor_get(snac *snac, const char *actor, d_char **data) | 1359 | int actor_get(snac *snac1, const char *actor, xs_dict **data) |
| 1284 | /* returns an already downloaded actor */ | 1360 | /* returns an already downloaded actor */ |
| 1285 | { | 1361 | { |
| 1286 | int status = 200; | 1362 | int status = 200; |
| 1287 | d_char *d; | 1363 | xs_dict *d = NULL; |
| 1288 | 1364 | ||
| 1289 | if (strcmp(actor, snac->actor) == 0) { | 1365 | if (strcmp(actor, snac1->actor) == 0) { |
| 1290 | /* this actor */ | 1366 | /* this actor */ |
| 1291 | if (data) | 1367 | if (data) |
| 1292 | *data = msg_actor(snac); | 1368 | *data = msg_actor(snac1); |
| 1293 | 1369 | ||
| 1294 | return status; | 1370 | return status; |
| 1295 | } | 1371 | } |
| 1296 | 1372 | ||
| 1373 | if (xs_startswith(actor, srv_baseurl)) { | ||
| 1374 | /* it's a (possible) local user */ | ||
| 1375 | xs *l = xs_split(actor, "/"); | ||
| 1376 | const char *uid = xs_list_get(l, -1); | ||
| 1377 | snac user; | ||
| 1378 | |||
| 1379 | if (!xs_is_null(uid) && user_open(&user, uid)) { | ||
| 1380 | if (data) | ||
| 1381 | *data = msg_actor(&user); | ||
| 1382 | |||
| 1383 | user_free(&user); | ||
| 1384 | return 200; | ||
| 1385 | } | ||
| 1386 | else | ||
| 1387 | return 404; | ||
| 1388 | } | ||
| 1389 | |||
| 1297 | /* read the object */ | 1390 | /* read the object */ |
| 1298 | if (!valid_status(status = object_get(actor, &d))) | 1391 | if (!valid_status(status = object_get(actor, &d))) { |
| 1392 | d = xs_free(d); | ||
| 1299 | return status; | 1393 | return status; |
| 1394 | } | ||
| 1300 | 1395 | ||
| 1301 | if (data) | 1396 | if (data) |
| 1302 | *data = d; | 1397 | *data = d; |
| @@ -1719,7 +1814,7 @@ static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries) | |||
| 1719 | } | 1814 | } |
| 1720 | 1815 | ||
| 1721 | 1816 | ||
| 1722 | void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries) | 1817 | void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries) |
| 1723 | /* enqueues an input message */ | 1818 | /* enqueues an input message */ |
| 1724 | { | 1819 | { |
| 1725 | xs *qmsg = _new_qmsg("input", msg, retries); | 1820 | xs *qmsg = _new_qmsg("input", msg, retries); |
| @@ -2020,7 +2115,11 @@ void purge_server(void) | |||
| 2020 | xs *ib_dir = xs_fmt("%s/inbox", srv_basedir); | 2115 | xs *ib_dir = xs_fmt("%s/inbox", srv_basedir); |
| 2021 | _purge_dir(ib_dir, 7); | 2116 | _purge_dir(ib_dir, 7); |
| 2022 | 2117 | ||
| 2023 | srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d)", cnt, icnt)); | 2118 | /* purge the instance timeline */ |
| 2119 | xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir); | ||
| 2120 | int itl_gc = index_gc(itl_fn); | ||
| 2121 | |||
| 2122 | srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d, itl: %d)", cnt, icnt, itl_gc)); | ||
| 2024 | } | 2123 | } |
| 2025 | 2124 | ||
| 2026 | 2125 | ||
| @@ -220,6 +220,36 @@ Please take note that they will show your timeline in a 'Mastodon fashion' | |||
| 220 | post display with the most active threads at the top that the web interface of | 220 | post display with the most active threads at the top that the web interface of |
| 221 | .Nm | 221 | .Nm |
| 222 | provides. | 222 | provides. |
| 223 | .Ss Implementing post bots | ||
| 224 | .Nm | ||
| 225 | makes very easy to post messages in a non-interactive manner. This example | ||
| 226 | posts a string: | ||
| 227 | .Bd -literal -offset indent | ||
| 228 | uptime | snac note $SNAC_BASEDIR $SNAC_USER - | ||
| 229 | .Ed | ||
| 230 | .Pp | ||
| 231 | You can setup a line like this from a | ||
| 232 | .Xr crontab 5 | ||
| 233 | or similar. Take note that you need a) command-line access to the same machine | ||
| 234 | that hosts the | ||
| 235 | .Nm | ||
| 236 | instance, and b) write permissions to the storage directories and files. | ||
| 237 | .Pp | ||
| 238 | You can also post non-interactively using the Mastodon API and a command-line | ||
| 239 | http tool like | ||
| 240 | .Xr curl 1 | ||
| 241 | or similar. This has the advantage that you can do it remotely from any host, | ||
| 242 | anywhere; the only thing you need is an API Token. This is an example: | ||
| 243 | .Bd -literal -offset indent | ||
| 244 | curl -X POST https://$SNAC_HOST/api/v1/statuses \\ | ||
| 245 | --header "Authorization: Bearer ${TOKEN}" -d "status=$(uptime)" | ||
| 246 | .Ed | ||
| 247 | .Pp | ||
| 248 | You can obtain an API Token by connecting to the following URL: | ||
| 249 | .Bd -literal -offset indent | ||
| 250 | https://$SNAC_HOST/oauth/x-snac-get-token | ||
| 251 | .Ed | ||
| 252 | .Pp | ||
| 223 | .Sh ENVIRONMENT | 253 | .Sh ENVIRONMENT |
| 224 | .Bl -tag -width Ds | 254 | .Bl -tag -width Ds |
| 225 | .It Ev DEBUG | 255 | .It Ev DEBUG |
diff --git a/examples/docker-entrypoint.sh b/examples/docker-entrypoint.sh index 639b692..a6216b2 100755 --- a/examples/docker-entrypoint.sh +++ b/examples/docker-entrypoint.sh | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | #! /bin/sh | 1 | #! /bin/sh |
| 2 | if [ ! -e /data/data/server.json ] | 2 | if [ ! -e /data/data/server.json ] |
| 3 | then | 3 | then |
| 4 | echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n" | snac init /data/data | 4 | echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n\r\n" | snac init /data/data |
| 5 | snac adduser /data/data testuser | 5 | snac adduser /data/data testuser |
| 6 | fi | 6 | fi |
| 7 | SSLKEYLOGFILE=/data/key snac httpd /data/data | 7 | SSLKEYLOGFILE=/data/key snac httpd /data/data |
| @@ -79,7 +79,7 @@ xs_str *actor_name(xs_dict *actor) | |||
| 79 | } | 79 | } |
| 80 | 80 | ||
| 81 | 81 | ||
| 82 | d_char *html_actor_icon(snac *snac, d_char *os, char *actor, | 82 | xs_str *html_actor_icon(xs_str *os, char *actor, |
| 83 | const char *date, const char *udate, const char *url, int priv) | 83 | const char *date, const char *udate, const char *url, int priv) |
| 84 | { | 84 | { |
| 85 | xs *s = xs_str_new(NULL); | 85 | xs *s = xs_str_new(NULL); |
| @@ -168,7 +168,7 @@ d_char *html_msg_icon(snac *snac, d_char *os, char *msg) | |||
| 168 | date = xs_dict_get(msg, "published"); | 168 | date = xs_dict_get(msg, "published"); |
| 169 | udate = xs_dict_get(msg, "updated"); | 169 | udate = xs_dict_get(msg, "updated"); |
| 170 | 170 | ||
| 171 | os = html_actor_icon(snac, os, actor, date, udate, url, priv); | 171 | os = html_actor_icon(os, actor, date, udate, url, priv); |
| 172 | } | 172 | } |
| 173 | 173 | ||
| 174 | return os; | 174 | return os; |
| @@ -983,7 +983,7 @@ d_char *html_entry(snac *snac, d_char *os, char *msg, int local, | |||
| 983 | } | 983 | } |
| 984 | 984 | ||
| 985 | 985 | ||
| 986 | d_char *html_user_footer(snac *snac, d_char *s) | 986 | xs_str *html_user_footer(xs_str *s) |
| 987 | { | 987 | { |
| 988 | xs *s1 = xs_fmt( | 988 | xs *s1 = xs_fmt( |
| 989 | "<div class=\"snac-footer\">\n" | 989 | "<div class=\"snac-footer\">\n" |
| @@ -1064,7 +1064,7 @@ d_char *html_timeline(snac *snac, char *list, int local, int skip, int show, int | |||
| 1064 | s = xs_str_cat(s, s1); | 1064 | s = xs_str_cat(s, s1); |
| 1065 | } | 1065 | } |
| 1066 | 1066 | ||
| 1067 | s = html_user_footer(snac, s); | 1067 | s = html_user_footer(s); |
| 1068 | 1068 | ||
| 1069 | s = xs_str_cat(s, "</body>\n</html>\n"); | 1069 | s = xs_str_cat(s, "</body>\n</html>\n"); |
| 1070 | 1070 | ||
| @@ -1088,8 +1088,7 @@ d_char *html_people_list(snac *snac, d_char *os, d_char *list, const char *heade | |||
| 1088 | if (valid_status(actor_get(snac, actor_id, &actor))) { | 1088 | if (valid_status(actor_get(snac, actor_id, &actor))) { |
| 1089 | s = xs_str_cat(s, "<div class=\"snac-post\">\n"); | 1089 | s = xs_str_cat(s, "<div class=\"snac-post\">\n"); |
| 1090 | 1090 | ||
| 1091 | s = html_actor_icon(snac, s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0); | 1091 | s = html_actor_icon(s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0); |
| 1092 | |||
| 1093 | 1092 | ||
| 1094 | /* content (user bio) */ | 1093 | /* content (user bio) */ |
| 1095 | char *c = xs_dict_get(actor, "summary"); | 1094 | char *c = xs_dict_get(actor, "summary"); |
| @@ -1182,7 +1181,7 @@ d_char *html_people(snac *snac) | |||
| 1182 | 1181 | ||
| 1183 | s = html_people_list(snac, s, wers, L("People that follows you"), "e"); | 1182 | s = html_people_list(snac, s, wers, L("People that follows you"), "e"); |
| 1184 | 1183 | ||
| 1185 | s = html_user_footer(snac, s); | 1184 | s = html_user_footer(s); |
| 1186 | 1185 | ||
| 1187 | s = xs_str_cat(s, "</body>\n</html>\n"); | 1186 | s = xs_str_cat(s, "</body>\n</html>\n"); |
| 1188 | 1187 | ||
| @@ -1226,7 +1225,7 @@ xs_str *html_notifications(snac *snac) | |||
| 1226 | const char *actor_id = xs_dict_get(noti, "actor"); | 1225 | const char *actor_id = xs_dict_get(noti, "actor"); |
| 1227 | xs *actor = NULL; | 1226 | xs *actor = NULL; |
| 1228 | 1227 | ||
| 1229 | if (!valid_status(object_get(actor_id, &actor))) | 1228 | if (!valid_status(actor_get(snac, actor_id, &actor))) |
| 1230 | continue; | 1229 | continue; |
| 1231 | 1230 | ||
| 1232 | xs *a_name = actor_name(actor); | 1231 | xs *a_name = actor_name(actor); |
| @@ -1277,12 +1276,13 @@ xs_str *html_notifications(snac *snac) | |||
| 1277 | s = xs_str_cat(s, s1); | 1276 | s = xs_str_cat(s, s1); |
| 1278 | } | 1277 | } |
| 1279 | 1278 | ||
| 1280 | s = html_user_footer(snac, s); | 1279 | s = html_user_footer(s); |
| 1281 | 1280 | ||
| 1282 | s = xs_str_cat(s, "</body>\n</html>\n"); | 1281 | s = xs_str_cat(s, "</body>\n</html>\n"); |
| 1283 | 1282 | ||
| 1284 | /* set the check time to now */ | 1283 | /* set the check time to now */ |
| 1285 | xs *dummy = notify_check_time(snac, 1); | 1284 | xs *dummy = notify_check_time(snac, 1); |
| 1285 | dummy = xs_free(dummy); | ||
| 1286 | 1286 | ||
| 1287 | timeline_touch(snac); | 1287 | timeline_touch(snac); |
| 1288 | 1288 | ||
| @@ -1290,7 +1290,8 @@ xs_str *html_notifications(snac *snac) | |||
| 1290 | } | 1290 | } |
| 1291 | 1291 | ||
| 1292 | 1292 | ||
| 1293 | int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype) | 1293 | int html_get_handler(const xs_dict *req, const char *q_path, |
| 1294 | char **body, int *b_size, char **ctype) | ||
| 1294 | { | 1295 | { |
| 1295 | char *accept = xs_dict_get(req, "accept"); | 1296 | char *accept = xs_dict_get(req, "accept"); |
| 1296 | int status = 404; | 1297 | int status = 404; |
| @@ -1547,9 +1548,13 @@ int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char * | |||
| 1547 | } | 1548 | } |
| 1548 | 1549 | ||
| 1549 | 1550 | ||
| 1550 | int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size, | 1551 | int html_post_handler(const xs_dict *req, const char *q_path, |
| 1552 | char *payload, int p_size, | ||
| 1551 | char **body, int *b_size, char **ctype) | 1553 | char **body, int *b_size, char **ctype) |
| 1552 | { | 1554 | { |
| 1555 | (void)p_size; | ||
| 1556 | (void)ctype; | ||
| 1557 | |||
| 1553 | int status = 0; | 1558 | int status = 0; |
| 1554 | snac snac; | 1559 | snac snac; |
| 1555 | char *uid, *p_path; | 1560 | char *uid, *p_path; |
| @@ -33,7 +33,7 @@ xs_dict *http_signed_request_raw(const char *keyid, const char *seckey, | |||
| 33 | date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT"); | 33 | date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT"); |
| 34 | 34 | ||
| 35 | { | 35 | { |
| 36 | xs *s = xs_replace(url, "https:/" "/", ""); | 36 | xs *s = xs_replace_n(url, "https:/" "/", "", 1); |
| 37 | l1 = xs_split_n(s, "/", 1); | 37 | l1 = xs_split_n(s, "/", 1); |
| 38 | } | 38 | } |
| 39 | 39 | ||
| @@ -43,12 +43,14 @@ d_char *nodeinfo_2_0(void) | |||
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | 45 | ||
| 46 | int server_get_handler(d_char *req, char *q_path, | 46 | int server_get_handler(xs_dict *req, char *q_path, |
| 47 | char **body, int *b_size, char **ctype) | 47 | char **body, int *b_size, char **ctype) |
| 48 | /* basic server services */ | 48 | /* basic server services */ |
| 49 | { | 49 | { |
| 50 | int status = 0; | 50 | int status = 0; |
| 51 | 51 | ||
| 52 | (void)req; | ||
| 53 | |||
| 52 | /* is it the server root? */ | 54 | /* is it the server root? */ |
| 53 | if (*q_path == '\0') { | 55 | if (*q_path == '\0') { |
| 54 | /* try to open greeting.html */ | 56 | /* try to open greeting.html */ |
| @@ -285,6 +287,8 @@ static jmp_buf on_break; | |||
| 285 | 287 | ||
| 286 | void term_handler(int s) | 288 | void term_handler(int s) |
| 287 | { | 289 | { |
| 290 | (void)s; | ||
| 291 | |||
| 288 | longjmp(on_break, 1); | 292 | longjmp(on_break, 1); |
| 289 | } | 293 | } |
| 290 | 294 | ||
| @@ -401,6 +405,8 @@ static void *background_thread(void *arg) | |||
| 401 | { | 405 | { |
| 402 | time_t purge_time; | 406 | time_t purge_time; |
| 403 | 407 | ||
| 408 | (void)arg; | ||
| 409 | |||
| 404 | /* first purge time */ | 410 | /* first purge time */ |
| 405 | purge_time = time(NULL) + 10 * 60; | 411 | purge_time = time(NULL) + 10 * 60; |
| 406 | 412 | ||
| @@ -30,6 +30,7 @@ int usage(void) | |||
| 30 | printf("actor {basedir} {uid} {url} Requests an actor\n"); | 30 | printf("actor {basedir} {uid} {url} Requests an actor\n"); |
| 31 | printf("note {basedir} {uid} {'text'} Sends a note to followers\n"); | 31 | printf("note {basedir} {uid} {'text'} Sends a note to followers\n"); |
| 32 | printf("resetpwd {basedir} {uid} Resets the password of a user\n"); | 32 | printf("resetpwd {basedir} {uid} Resets the password of a user\n"); |
| 33 | printf("ping {basedir} {uid} {actor} Pings an actor\n"); | ||
| 33 | 34 | ||
| 34 | return 1; | 35 | return 1; |
| 35 | } | 36 | } |
| @@ -228,6 +229,27 @@ int main(int argc, char *argv[]) | |||
| 228 | return 0; | 229 | return 0; |
| 229 | } | 230 | } |
| 230 | 231 | ||
| 232 | if (strcmp(cmd, "ping") == 0) { | ||
| 233 | xs *actor_o = NULL; | ||
| 234 | |||
| 235 | if (valid_status(actor_request(&snac, url, &actor_o))) { | ||
| 236 | xs *msg = msg_ping(&snac, url); | ||
| 237 | |||
| 238 | enqueue_output_by_actor(&snac, msg, url, 0); | ||
| 239 | |||
| 240 | if (dbglevel) { | ||
| 241 | xs *j = xs_json_dumps_pp(msg, 4); | ||
| 242 | printf("%s\n", j); | ||
| 243 | } | ||
| 244 | } | ||
| 245 | else { | ||
| 246 | srv_log(xs_fmt("Error getting actor %s", url)); | ||
| 247 | return 1; | ||
| 248 | } | ||
| 249 | |||
| 250 | return 0; | ||
| 251 | } | ||
| 252 | |||
| 231 | if (strcmp(cmd, "request") == 0) { | 253 | if (strcmp(cmd, "request") == 0) { |
| 232 | int status; | 254 | int status; |
| 233 | xs *data = NULL; | 255 | xs *data = NULL; |
| @@ -162,7 +162,7 @@ const char *login_page = "" | |||
| 162 | "<!DOCTYPE html>\n" | 162 | "<!DOCTYPE html>\n" |
| 163 | "<body><h1>%s OAuth identify</h1>\n" | 163 | "<body><h1>%s OAuth identify</h1>\n" |
| 164 | "<div style=\"background-color: red; color: white\">%s</div>\n" | 164 | "<div style=\"background-color: red; color: white\">%s</div>\n" |
| 165 | "<form method=\"post\" action=\"https:/" "/%s/oauth/x-snac-login\">\n" | 165 | "<form method=\"post\" action=\"https:/" "/%s/%s\">\n" |
| 166 | "<p>Login: <input type=\"text\" name=\"login\"></p>\n" | 166 | "<p>Login: <input type=\"text\" name=\"login\"></p>\n" |
| 167 | "<p>Password: <input type=\"password\" name=\"passwd\"></p>\n" | 167 | "<p>Password: <input type=\"password\" name=\"passwd\"></p>\n" |
| 168 | "<input type=\"hidden\" name=\"redir\" value=\"%s\">\n" | 168 | "<input type=\"hidden\" name=\"redir\" value=\"%s\">\n" |
| @@ -175,6 +175,8 @@ const char *login_page = "" | |||
| 175 | int oauth_get_handler(const xs_dict *req, const char *q_path, | 175 | int oauth_get_handler(const xs_dict *req, const char *q_path, |
| 176 | char **body, int *b_size, char **ctype) | 176 | char **body, int *b_size, char **ctype) |
| 177 | { | 177 | { |
| 178 | (void)b_size; | ||
| 179 | |||
| 178 | if (!xs_startswith(q_path, "/oauth/")) | 180 | if (!xs_startswith(q_path, "/oauth/")) |
| 179 | return 0; | 181 | return 0; |
| 180 | 182 | ||
| @@ -185,7 +187,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path, | |||
| 185 | 187 | ||
| 186 | int status = 404; | 188 | int status = 404; |
| 187 | xs_dict *msg = xs_dict_get(req, "q_vars"); | 189 | xs_dict *msg = xs_dict_get(req, "q_vars"); |
| 188 | xs *cmd = xs_replace(q_path, "/oauth", ""); | 190 | xs *cmd = xs_replace_n(q_path, "/oauth", "", 1); |
| 189 | 191 | ||
| 190 | srv_debug(1, xs_fmt("oauth_get_handler %s", q_path)); | 192 | srv_debug(1, xs_fmt("oauth_get_handler %s", q_path)); |
| 191 | 193 | ||
| @@ -206,17 +208,28 @@ int oauth_get_handler(const xs_dict *req, const char *q_path, | |||
| 206 | if (xs_is_null(state)) | 208 | if (xs_is_null(state)) |
| 207 | state = ""; | 209 | state = ""; |
| 208 | 210 | ||
| 209 | *body = xs_fmt(login_page, host, "", host, ruri, cid, state, USER_AGENT); | 211 | *body = xs_fmt(login_page, host, "", host, "oauth/x-snac-login", |
| 212 | ruri, cid, state, USER_AGENT); | ||
| 210 | *ctype = "text/html"; | 213 | *ctype = "text/html"; |
| 211 | status = 200; | 214 | status = 200; |
| 212 | 215 | ||
| 213 | srv_debug(0, xs_fmt("oauth authorize: generating login page")); | 216 | srv_debug(1, xs_fmt("oauth authorize: generating login page")); |
| 214 | } | 217 | } |
| 215 | else | 218 | else |
| 216 | srv_debug(0, xs_fmt("oauth authorize: bad client_id %s", cid)); | 219 | srv_debug(1, xs_fmt("oauth authorize: bad client_id %s", cid)); |
| 217 | } | 220 | } |
| 218 | else | 221 | else |
| 219 | srv_debug(0, xs_fmt("oauth authorize: invalid or unset arguments")); | 222 | srv_debug(1, xs_fmt("oauth authorize: invalid or unset arguments")); |
| 223 | } | ||
| 224 | else | ||
| 225 | if (strcmp(cmd, "/x-snac-get-token") == 0) { | ||
| 226 | const char *host = xs_dict_get(srv_config, "host"); | ||
| 227 | |||
| 228 | *body = xs_fmt(login_page, host, "", host, "oauth/x-snac-get-token", | ||
| 229 | "", "", "", USER_AGENT); | ||
| 230 | *ctype = "text/html"; | ||
| 231 | status = 200; | ||
| 232 | |||
| 220 | } | 233 | } |
| 221 | 234 | ||
| 222 | return status; | 235 | return status; |
| @@ -227,6 +240,9 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 227 | const char *payload, int p_size, | 240 | const char *payload, int p_size, |
| 228 | char **body, int *b_size, char **ctype) | 241 | char **body, int *b_size, char **ctype) |
| 229 | { | 242 | { |
| 243 | (void)p_size; | ||
| 244 | (void)b_size; | ||
| 245 | |||
| 230 | if (!xs_startswith(q_path, "/oauth/")) | 246 | if (!xs_startswith(q_path, "/oauth/")) |
| 231 | return 0; | 247 | return 0; |
| 232 | 248 | ||
| @@ -245,7 +261,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 245 | else | 261 | else |
| 246 | args = xs_dup(xs_dict_get(req, "p_vars")); | 262 | args = xs_dup(xs_dict_get(req, "p_vars")); |
| 247 | 263 | ||
| 248 | xs *cmd = xs_replace(q_path, "/oauth", ""); | 264 | xs *cmd = xs_replace_n(q_path, "/oauth", "", 1); |
| 249 | 265 | ||
| 250 | srv_debug(1, xs_fmt("oauth_post_handler %s", q_path)); | 266 | srv_debug(1, xs_fmt("oauth_post_handler %s", q_path)); |
| 251 | 267 | ||
| @@ -259,7 +275,8 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 259 | const char *host = xs_dict_get(srv_config, "host"); | 275 | const char *host = xs_dict_get(srv_config, "host"); |
| 260 | 276 | ||
| 261 | /* by default, generate another login form with an error */ | 277 | /* by default, generate another login form with an error */ |
| 262 | *body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, redir, cid, state, USER_AGENT); | 278 | *body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-login", |
| 279 | redir, cid, state, USER_AGENT); | ||
| 263 | *ctype = "text/html"; | 280 | *ctype = "text/html"; |
| 264 | status = 200; | 281 | status = 200; |
| 265 | 282 | ||
| @@ -268,8 +285,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 268 | 285 | ||
| 269 | if (user_open(&snac, login)) { | 286 | if (user_open(&snac, login)) { |
| 270 | /* check the login + password */ | 287 | /* check the login + password */ |
| 271 | if (check_password(login, passwd, | 288 | if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) { |
| 272 | xs_dict_get(snac.config, "passwd"))) { | ||
| 273 | /* success! redirect to the desired uri */ | 289 | /* success! redirect to the desired uri */ |
| 274 | xs *code = random_str(); | 290 | xs *code = random_str(); |
| 275 | 291 | ||
| @@ -328,7 +344,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 328 | const char *auhdr = xs_dict_get(req, "authorization"); | 344 | const char *auhdr = xs_dict_get(req, "authorization"); |
| 329 | 345 | ||
| 330 | if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) { | 346 | if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) { |
| 331 | xs *s1 = xs_replace(auhdr, "Basic ", ""); | 347 | xs *s1 = xs_replace_n(auhdr, "Basic ", "", 1); |
| 332 | int size; | 348 | int size; |
| 333 | xs *s2 = xs_base64_dec(s1, &size); | 349 | xs *s2 = xs_base64_dec(s1, &size); |
| 334 | 350 | ||
| @@ -373,7 +389,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 373 | 389 | ||
| 374 | const char *uid = xs_dict_get(app, "uid"); | 390 | const char *uid = xs_dict_get(app, "uid"); |
| 375 | 391 | ||
| 376 | srv_debug(0, xs_fmt("oauth token: " | 392 | srv_debug(1, xs_fmt("oauth token: " |
| 377 | "successful login for %s, new token %s", uid, tokid)); | 393 | "successful login for %s, new token %s", uid, tokid)); |
| 378 | 394 | ||
| 379 | xs *token = xs_dict_new(); | 395 | xs *token = xs_dict_new(); |
| @@ -387,7 +403,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 387 | } | 403 | } |
| 388 | } | 404 | } |
| 389 | else { | 405 | else { |
| 390 | srv_debug(0, xs_fmt("oauth token: invalid or unset arguments")); | 406 | srv_debug(1, xs_fmt("oauth token: invalid or unset arguments")); |
| 391 | status = 400; | 407 | status = 400; |
| 392 | } | 408 | } |
| 393 | } | 409 | } |
| @@ -409,7 +425,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 409 | } | 425 | } |
| 410 | else { | 426 | else { |
| 411 | token_del(tokid); | 427 | token_del(tokid); |
| 412 | srv_debug(0, xs_fmt("oauth revoke: revoked token %s", tokid)); | 428 | srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid)); |
| 413 | status = 200; | 429 | status = 200; |
| 414 | 430 | ||
| 415 | /* also delete the app, as it serves no purpose from now on */ | 431 | /* also delete the app, as it serves no purpose from now on */ |
| @@ -417,10 +433,52 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 417 | } | 433 | } |
| 418 | } | 434 | } |
| 419 | else { | 435 | else { |
| 420 | srv_debug(0, xs_fmt("oauth revoke: invalid or unset arguments")); | 436 | srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments")); |
| 421 | status = 403; | 437 | status = 403; |
| 422 | } | 438 | } |
| 423 | } | 439 | } |
| 440 | if (strcmp(cmd, "/x-snac-get-token") == 0) { | ||
| 441 | const char *login = xs_dict_get(args, "login"); | ||
| 442 | const char *passwd = xs_dict_get(args, "passwd"); | ||
| 443 | |||
| 444 | const char *host = xs_dict_get(srv_config, "host"); | ||
| 445 | |||
| 446 | /* by default, generate another login form with an error */ | ||
| 447 | *body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-get-token", | ||
| 448 | "", "", "", USER_AGENT); | ||
| 449 | *ctype = "text/html"; | ||
| 450 | status = 200; | ||
| 451 | |||
| 452 | if (login && passwd) { | ||
| 453 | snac user; | ||
| 454 | |||
| 455 | if (user_open(&user, login)) { | ||
| 456 | /* check the login + password */ | ||
| 457 | if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) { | ||
| 458 | /* success! create a new token */ | ||
| 459 | xs *tokid = random_str(); | ||
| 460 | |||
| 461 | srv_debug(1, xs_fmt("x-snac-new-token: " | ||
| 462 | "successful login for %s, new token %s", login, tokid)); | ||
| 463 | |||
| 464 | xs *token = xs_dict_new(); | ||
| 465 | token = xs_dict_append(token, "token", tokid); | ||
| 466 | token = xs_dict_append(token, "client_id", "snac-client"); | ||
| 467 | token = xs_dict_append(token, "client_secret", ""); | ||
| 468 | token = xs_dict_append(token, "uid", login); | ||
| 469 | token = xs_dict_append(token, "code", ""); | ||
| 470 | |||
| 471 | token_add(tokid, token); | ||
| 472 | |||
| 473 | *ctype = "text/plain"; | ||
| 474 | xs_free(*body); | ||
| 475 | *body = xs_dup(tokid); | ||
| 476 | } | ||
| 477 | |||
| 478 | user_free(&user); | ||
| 479 | } | ||
| 480 | } | ||
| 481 | } | ||
| 424 | 482 | ||
| 425 | return status; | 483 | return status; |
| 426 | } | 484 | } |
| @@ -537,7 +595,6 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) | |||
| 537 | xs *f = xs_val_new(XSTYPE_FALSE); | 595 | xs *f = xs_val_new(XSTYPE_FALSE); |
| 538 | xs *t = xs_val_new(XSTYPE_TRUE); | 596 | xs *t = xs_val_new(XSTYPE_TRUE); |
| 539 | xs *n = xs_val_new(XSTYPE_NULL); | 597 | xs *n = xs_val_new(XSTYPE_NULL); |
| 540 | xs *el = xs_list_new(); | ||
| 541 | xs *idx = NULL; | 598 | xs *idx = NULL; |
| 542 | xs *ixc = NULL; | 599 | xs *ixc = NULL; |
| 543 | 600 | ||
| @@ -787,7 +844,7 @@ int process_auth_token(snac *snac, const xs_dict *req) | |||
| 787 | 844 | ||
| 788 | /* if there is an authorization field, try to validate it */ | 845 | /* if there is an authorization field, try to validate it */ |
| 789 | if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) { | 846 | if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) { |
| 790 | xs *tokid = xs_replace(v, "Bearer ", ""); | 847 | xs *tokid = xs_replace_n(v, "Bearer ", "", 1); |
| 791 | xs *token = token_get(tokid); | 848 | xs *token = token_get(tokid); |
| 792 | 849 | ||
| 793 | if (token != NULL) { | 850 | if (token != NULL) { |
| @@ -815,6 +872,8 @@ int process_auth_token(snac *snac, const xs_dict *req) | |||
| 815 | int mastoapi_get_handler(const xs_dict *req, const char *q_path, | 872 | int mastoapi_get_handler(const xs_dict *req, const char *q_path, |
| 816 | char **body, int *b_size, char **ctype) | 873 | char **body, int *b_size, char **ctype) |
| 817 | { | 874 | { |
| 875 | (void)b_size; | ||
| 876 | |||
| 818 | if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) | 877 | if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) |
| 819 | return 0; | 878 | return 0; |
| 820 | 879 | ||
| @@ -826,7 +885,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 826 | 885 | ||
| 827 | int status = 404; | 886 | int status = 404; |
| 828 | xs_dict *args = xs_dict_get(req, "q_vars"); | 887 | xs_dict *args = xs_dict_get(req, "q_vars"); |
| 829 | xs *cmd = xs_replace(q_path, "/api", ""); | 888 | xs *cmd = xs_replace_n(q_path, "/api", "", 1); |
| 830 | 889 | ||
| 831 | snac snac1 = {0}; | 890 | snac snac1 = {0}; |
| 832 | int logged_in = process_auth_token(&snac1, req); | 891 | int logged_in = process_auth_token(&snac1, req); |
| @@ -1036,11 +1095,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1036 | } | 1095 | } |
| 1037 | else | 1096 | else |
| 1038 | if (strcmp(cmd, "/v1/timelines/public") == 0) { | 1097 | if (strcmp(cmd, "/v1/timelines/public") == 0) { |
| 1039 | /* the public timeline (public timelines for all users) */ | 1098 | /* the instance public timeline (public timelines for all users) */ |
| 1040 | 1099 | ||
| 1041 | /* this is an ugly kludge: first users in the list get all the fame */ | 1100 | /* NOTE: this api call needs no authorization; but, |
| 1101 | I need a logged-in user in mastoapi_status() for | ||
| 1102 | is_msg_public() and the liked/boosted flags, | ||
| 1103 | so it will silently fail for pure public access */ | ||
| 1042 | 1104 | ||
| 1043 | const char *limit_s = xs_dict_get(args, "limit"); | 1105 | const char *limit_s = xs_dict_get(args, "limit"); |
| 1044 | int limit = 0; | 1106 | int limit = 0; |
| 1045 | int cnt = 0; | 1107 | int cnt = 0; |
| 1046 | 1108 | ||
| @@ -1050,44 +1112,28 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1050 | if (limit == 0) | 1112 | if (limit == 0) |
| 1051 | limit = 20; | 1113 | limit = 20; |
| 1052 | 1114 | ||
| 1053 | xs *out = xs_list_new(); | 1115 | xs *timeline = timeline_instance_list(0, limit); |
| 1054 | xs *users = user_list(); | 1116 | xs *out = xs_list_new(); |
| 1055 | xs_list *p = users; | 1117 | xs_list *p = timeline; |
| 1056 | xs_str *uid; | 1118 | xs_str *md5; |
| 1057 | |||
| 1058 | while (xs_list_iter(&p, &uid) && cnt < limit) { | ||
| 1059 | snac user; | ||
| 1060 | |||
| 1061 | if (user_open(&user, uid)) { | ||
| 1062 | xs *timeline = timeline_simple_list(&user, "public", 0, 4); | ||
| 1063 | xs_list *p2 = timeline; | ||
| 1064 | xs_str *v; | ||
| 1065 | |||
| 1066 | while (xs_list_iter(&p2, &v) && cnt < limit) { | ||
| 1067 | xs *msg = NULL; | ||
| 1068 | 1119 | ||
| 1069 | /* get the entry */ | 1120 | while (logged_in && xs_list_iter(&p, &md5) && cnt < limit) { |
| 1070 | if (!valid_status(timeline_get_by_md5(&user, v, &msg))) | 1121 | xs *msg = NULL; |
| 1071 | continue; | ||
| 1072 | |||
| 1073 | /* discard non-Notes */ | ||
| 1074 | if (strcmp(xs_dict_get(msg, "type"), "Note") != 0) | ||
| 1075 | continue; | ||
| 1076 | 1122 | ||
| 1077 | /* discard entries not by this user */ | 1123 | /* get the entry */ |
| 1078 | if (!xs_startswith(xs_dict_get(msg, "id"), user.actor)) | 1124 | if (!valid_status(object_get_by_md5(md5, &msg))) |
| 1079 | continue; | 1125 | continue; |
| 1080 | 1126 | ||
| 1081 | /* convert the Note into a Mastodon status */ | 1127 | /* discard non-Notes */ |
| 1082 | xs *st = mastoapi_status(&user, msg); | 1128 | if (strcmp(xs_dict_get(msg, "type"), "Note") != 0) |
| 1129 | continue; | ||
| 1083 | 1130 | ||
| 1084 | if (st != NULL) { | 1131 | /* convert the Note into a Mastodon status */ |
| 1085 | out = xs_list_append(out, st); | 1132 | xs *st = mastoapi_status(&snac1, msg); |
| 1086 | cnt++; | ||
| 1087 | } | ||
| 1088 | } | ||
| 1089 | 1133 | ||
| 1090 | user_free(&user); | 1134 | if (st != NULL) { |
| 1135 | out = xs_list_append(out, st); | ||
| 1136 | cnt++; | ||
| 1091 | } | 1137 | } |
| 1092 | } | 1138 | } |
| 1093 | 1139 | ||
| @@ -1121,7 +1167,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1121 | xs *actor = NULL; | 1167 | xs *actor = NULL; |
| 1122 | xs *entry = NULL; | 1168 | xs *entry = NULL; |
| 1123 | 1169 | ||
| 1124 | if (!valid_status(object_get(xs_dict_get(noti, "actor"), &actor))) | 1170 | if (!valid_status(actor_get(&snac1, xs_dict_get(noti, "actor"), &actor))) |
| 1125 | continue; | 1171 | continue; |
| 1126 | 1172 | ||
| 1127 | if (objid != NULL && !valid_status(object_get(objid, &entry))) | 1173 | if (objid != NULL && !valid_status(object_get(objid, &entry))) |
| @@ -1287,25 +1333,6 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1287 | cfg = xs_dict_append(cfg, "statuses", d11); | 1333 | cfg = xs_dict_append(cfg, "statuses", d11); |
| 1288 | } | 1334 | } |
| 1289 | 1335 | ||
| 1290 | { | ||
| 1291 | xs *d11 = xs_dict_new(); | ||
| 1292 | xs *mt = xs_list_new(); | ||
| 1293 | |||
| 1294 | mt = xs_list_append(mt, "image/jpeg"); | ||
| 1295 | mt = xs_list_append(mt, "image/png"); | ||
| 1296 | mt = xs_list_append(mt, "image/gif"); | ||
| 1297 | |||
| 1298 | d11 = xs_dict_append(d11, "supported_mime_types", mt); | ||
| 1299 | |||
| 1300 | d11 = xs_dict_append(d11, "image_size_limit", z); | ||
| 1301 | d11 = xs_dict_append(d11, "image_matrix_limit", z); | ||
| 1302 | d11 = xs_dict_append(d11, "video_size_limit", z); | ||
| 1303 | d11 = xs_dict_append(d11, "video_matrix_limit", z); | ||
| 1304 | d11 = xs_dict_append(d11, "video_frame_rate_limit", z); | ||
| 1305 | |||
| 1306 | cfg = xs_dict_append(cfg, "media_attachments", d11); | ||
| 1307 | } | ||
| 1308 | |||
| 1309 | ins = xs_dict_append(ins, "configuration", cfg); | 1336 | ins = xs_dict_append(ins, "configuration", cfg); |
| 1310 | 1337 | ||
| 1311 | *body = xs_json_dumps_pp(ins, 4); | 1338 | *body = xs_json_dumps_pp(ins, 4); |
| @@ -1326,7 +1353,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1326 | /* skip the 'fake' part of the id */ | 1353 | /* skip the 'fake' part of the id */ |
| 1327 | id = MID_TO_MD5(id); | 1354 | id = MID_TO_MD5(id); |
| 1328 | 1355 | ||
| 1329 | if (valid_status(timeline_get_by_md5(&snac1, id, &msg))) { | 1356 | if (valid_status(object_get_by_md5(id, &msg))) { |
| 1330 | if (op == NULL) { | 1357 | if (op == NULL) { |
| 1331 | if (!is_muted(&snac1, xs_dict_get(msg, "attributedTo"))) { | 1358 | if (!is_muted(&snac1, xs_dict_get(msg, "attributedTo"))) { |
| 1332 | /* return the status itself */ | 1359 | /* return the status itself */ |
| @@ -1487,6 +1514,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 1487 | const char *payload, int p_size, | 1514 | const char *payload, int p_size, |
| 1488 | char **body, int *b_size, char **ctype) | 1515 | char **body, int *b_size, char **ctype) |
| 1489 | { | 1516 | { |
| 1517 | (void)p_size; | ||
| 1518 | (void)b_size; | ||
| 1519 | |||
| 1490 | if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) | 1520 | if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) |
| 1491 | return 0; | 1521 | return 0; |
| 1492 | 1522 | ||
| @@ -1513,7 +1543,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 1513 | printf("%s\n", j); | 1543 | printf("%s\n", j); |
| 1514 | }*/ | 1544 | }*/ |
| 1515 | 1545 | ||
| 1516 | xs *cmd = xs_replace(q_path, "/api", ""); | 1546 | xs *cmd = xs_replace_n(q_path, "/api", "", 1); |
| 1517 | 1547 | ||
| 1518 | snac snac = {0}; | 1548 | snac snac = {0}; |
| 1519 | int logged_in = process_auth_token(&snac, req); | 1549 | int logged_in = process_auth_token(&snac, req); |
| @@ -1562,7 +1592,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 1562 | 1592 | ||
| 1563 | app_add(cid, app); | 1593 | app_add(cid, app); |
| 1564 | 1594 | ||
| 1565 | srv_debug(0, xs_fmt("mastoapi apps: new app %s", cid)); | 1595 | srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid)); |
| 1566 | } | 1596 | } |
| 1567 | } | 1597 | } |
| 1568 | else | 1598 | else |
| @@ -1582,6 +1612,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 1582 | if (xs_is_null(media_ids)) | 1612 | if (xs_is_null(media_ids)) |
| 1583 | media_ids = xs_dict_get(args, "media_ids[]"); | 1613 | media_ids = xs_dict_get(args, "media_ids[]"); |
| 1584 | 1614 | ||
| 1615 | if (xs_is_null(visibility)) | ||
| 1616 | visibility = "public"; | ||
| 1617 | |||
| 1585 | xs *attach_list = xs_list_new(); | 1618 | xs *attach_list = xs_list_new(); |
| 1586 | xs *irt = NULL; | 1619 | xs *irt = NULL; |
| 1587 | 1620 | ||
| @@ -1683,7 +1716,11 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 1683 | } | 1716 | } |
| 1684 | else | 1717 | else |
| 1685 | if (strcmp(op, "unfavourite") == 0) { | 1718 | if (strcmp(op, "unfavourite") == 0) { |
| 1686 | /* snac does not support Undo+Like */ | 1719 | /* partial support: as the original Like message |
| 1720 | is not stored anywhere here, it's not possible | ||
| 1721 | to send an Undo + Like; the only thing done here | ||
| 1722 | is to delete the actor from the list of likes */ | ||
| 1723 | object_unadmire(id, snac.actor, 1); | ||
| 1687 | } | 1724 | } |
| 1688 | else | 1725 | else |
| 1689 | if (strcmp(op, "reblog") == 0) { | 1726 | if (strcmp(op, "reblog") == 0) { |
| @@ -1698,7 +1735,8 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 1698 | } | 1735 | } |
| 1699 | else | 1736 | else |
| 1700 | if (strcmp(op, "unreblog") == 0) { | 1737 | if (strcmp(op, "unreblog") == 0) { |
| 1701 | /* snac does not support Undo+Announce */ | 1738 | /* partial support: see comment in 'unfavourite' */ |
| 1739 | object_unadmire(id, snac.actor, 0); | ||
| 1702 | } | 1740 | } |
| 1703 | else | 1741 | else |
| 1704 | if (strcmp(op, "bookmark") == 0) { | 1742 | if (strcmp(op, "bookmark") == 0) { |
| @@ -1903,6 +1941,9 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path, | |||
| 1903 | const char *payload, int p_size, | 1941 | const char *payload, int p_size, |
| 1904 | char **body, int *b_size, char **ctype) | 1942 | char **body, int *b_size, char **ctype) |
| 1905 | { | 1943 | { |
| 1944 | (void)p_size; | ||
| 1945 | (void)b_size; | ||
| 1946 | |||
| 1906 | if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) | 1947 | if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) |
| 1907 | return 0; | 1948 | return 0; |
| 1908 | 1949 | ||
| @@ -1924,7 +1965,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path, | |||
| 1924 | if (args == NULL) | 1965 | if (args == NULL) |
| 1925 | return 400; | 1966 | return 400; |
| 1926 | 1967 | ||
| 1927 | xs *cmd = xs_replace(q_path, "/api", ""); | 1968 | xs *cmd = xs_replace_n(q_path, "/api", "", 1); |
| 1928 | 1969 | ||
| 1929 | snac snac = {0}; | 1970 | snac snac = {0}; |
| 1930 | int logged_in = process_auth_token(&snac, req); | 1971 | int logged_in = process_auth_token(&snac, req); |
| @@ -1,7 +1,7 @@ | |||
| 1 | /* snac - A simple, minimalistic ActivityPub instance */ | 1 | /* snac - A simple, minimalistic ActivityPub instance */ |
| 2 | /* copyright (c) 2022 - 2023 grunfink / MIT license */ | 2 | /* copyright (c) 2022 - 2023 grunfink / MIT license */ |
| 3 | 3 | ||
| 4 | #define VERSION "2.29" | 4 | #define VERSION "2.30" |
| 5 | 5 | ||
| 6 | #define USER_AGENT "snac/" VERSION | 6 | #define USER_AGENT "snac/" VERSION |
| 7 | 7 | ||
| @@ -83,6 +83,7 @@ int object_del_if_unref(const char *id); | |||
| 83 | double object_ctime_by_md5(const char *md5); | 83 | double object_ctime_by_md5(const char *md5); |
| 84 | double object_ctime(const char *id); | 84 | double object_ctime(const char *id); |
| 85 | int object_admire(const char *id, const char *actor, int like); | 85 | int object_admire(const char *id, const char *actor, int like); |
| 86 | int object_unadmire(const char *id, const char *actor, int like); | ||
| 86 | 87 | ||
| 87 | int object_likes_len(const char *id); | 88 | int object_likes_len(const char *id); |
| 88 | int object_announces_len(const char *id); | 89 | int object_announces_len(const char *id); |
| @@ -105,14 +106,14 @@ int timeline_touch(snac *snac); | |||
| 105 | int timeline_here(snac *snac, const char *md5); | 106 | int timeline_here(snac *snac, const char *md5); |
| 106 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); | 107 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); |
| 107 | int timeline_del(snac *snac, char *id); | 108 | int timeline_del(snac *snac, char *id); |
| 108 | d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show); | 109 | xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show); |
| 109 | d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show); | 110 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show); |
| 110 | int timeline_add(snac *snac, char *id, char *o_msg); | 111 | int timeline_add(snac *snac, char *id, char *o_msg); |
| 111 | void timeline_admire(snac *snac, char *id, char *admirer, int like); | 112 | void timeline_admire(snac *snac, char *id, char *admirer, int like); |
| 112 | 113 | ||
| 113 | xs_list *timeline_top_level(snac *snac, xs_list *list); | 114 | xs_list *timeline_top_level(snac *snac, xs_list *list); |
| 114 | 115 | xs_list *local_list(snac *snac, int max); | |
| 115 | d_char *local_list(snac *snac, int max); | 116 | xs_list *timeline_instance_list(int skip, int show); |
| 116 | 117 | ||
| 117 | int following_add(snac *snac, const char *actor, const xs_dict *msg); | 118 | int following_add(snac *snac, const char *actor, const xs_dict *msg); |
| 118 | int following_del(snac *snac, const char *actor); | 119 | int following_del(snac *snac, const char *actor); |
| @@ -127,8 +128,8 @@ int is_muted(snac *snac, const char *actor); | |||
| 127 | void hide(snac *snac, const char *id); | 128 | void hide(snac *snac, const char *id); |
| 128 | int is_hidden(snac *snac, const char *id); | 129 | int is_hidden(snac *snac, const char *id); |
| 129 | 130 | ||
| 130 | int actor_add(snac *snac, const char *actor, d_char *msg); | 131 | int actor_add(const char *actor, xs_dict *msg); |
| 131 | int actor_get(snac *snac, const char *actor, d_char **data); | 132 | int actor_get(snac *snac, const char *actor, xs_dict **data); |
| 132 | 133 | ||
| 133 | int static_get(snac *snac, const char *id, d_char **data, int *size); | 134 | int static_get(snac *snac, const char *id, d_char **data, int *size); |
| 134 | void static_put(snac *snac, const char *id, const char *data, int size); | 135 | void static_put(snac *snac, const char *id, const char *data, int size); |
| @@ -154,7 +155,7 @@ void inbox_add(const char *inbox); | |||
| 154 | void inbox_add_by_actor(const xs_dict *actor); | 155 | void inbox_add_by_actor(const xs_dict *actor); |
| 155 | xs_list *inbox_list(void); | 156 | xs_list *inbox_list(void); |
| 156 | 157 | ||
| 157 | void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries); | 158 | void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries); |
| 158 | void enqueue_output_raw(const char *keyid, const char *seckey, | 159 | void enqueue_output_raw(const char *keyid, const char *seckey, |
| 159 | xs_dict *msg, xs_str *inbox, int retries); | 160 | xs_dict *msg, xs_str *inbox, int retries); |
| 160 | void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries); | 161 | void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries); |
| @@ -186,7 +187,7 @@ int check_signature(snac *snac, xs_dict *req, xs_str **err); | |||
| 186 | void httpd(void); | 187 | void httpd(void); |
| 187 | 188 | ||
| 188 | int webfinger_request(const char *qs, char **actor, char **user); | 189 | int webfinger_request(const char *qs, char **actor, char **user); |
| 189 | int webfinger_get_handler(d_char *req, char *q_path, | 190 | int webfinger_get_handler(xs_dict *req, char *q_path, |
| 190 | char **body, int *b_size, char **ctype); | 191 | char **body, int *b_size, char **ctype); |
| 191 | 192 | ||
| 192 | const char *default_avatar_base64(void); | 193 | const char *default_avatar_base64(void); |
| @@ -202,6 +203,8 @@ d_char *msg_undo(snac *snac, char *object); | |||
| 202 | d_char *msg_delete(snac *snac, char *id); | 203 | d_char *msg_delete(snac *snac, char *id); |
| 203 | d_char *msg_actor(snac *snac); | 204 | d_char *msg_actor(snac *snac); |
| 204 | xs_dict *msg_update(snac *snac, xs_dict *object); | 205 | xs_dict *msg_update(snac *snac, xs_dict *object); |
| 206 | xs_dict *msg_ping(snac *user, const char *rcpt); | ||
| 207 | xs_dict *msg_pong(snac *user, const char *rcpt, const char *object); | ||
| 205 | 208 | ||
| 206 | int activitypub_request(snac *snac, const char *url, xs_dict **data); | 209 | int activitypub_request(snac *snac, const char *url, xs_dict **data); |
| 207 | int actor_request(snac *snac, const char *actor, xs_dict **data); | 210 | int actor_request(snac *snac, const char *actor, xs_dict **data); |
| @@ -219,17 +222,19 @@ int process_user_queue(snac *snac); | |||
| 219 | void process_queue_item(xs_dict *q_item); | 222 | void process_queue_item(xs_dict *q_item); |
| 220 | int process_queue(void); | 223 | int process_queue(void); |
| 221 | 224 | ||
| 222 | int activitypub_get_handler(d_char *req, char *q_path, | 225 | int activitypub_get_handler(const xs_dict *req, const char *q_path, |
| 223 | char **body, int *b_size, char **ctype); | 226 | char **body, int *b_size, char **ctype); |
| 224 | int activitypub_post_handler(d_char *req, char *q_path, | 227 | int activitypub_post_handler(const xs_dict *req, const char *q_path, |
| 225 | char *payload, int p_size, | 228 | char *payload, int p_size, |
| 226 | char **body, int *b_size, char **ctype); | 229 | char **body, int *b_size, char **ctype); |
| 227 | 230 | ||
| 228 | d_char *not_really_markdown(const char *content); | 231 | d_char *not_really_markdown(const char *content); |
| 229 | d_char *sanitize(const char *str); | 232 | d_char *sanitize(const char *str); |
| 230 | 233 | ||
| 231 | int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype); | 234 | int html_get_handler(const xs_dict *req, const char *q_path, |
| 232 | int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size, | 235 | char **body, int *b_size, char **ctype); |
| 236 | int html_post_handler(const xs_dict *req, const char *q_path, | ||
| 237 | char *payload, int p_size, | ||
| 233 | char **body, int *b_size, char **ctype); | 238 | char **body, int *b_size, char **ctype); |
| 234 | 239 | ||
| 235 | int snac_init(const char *_basedir); | 240 | int snac_init(const char *_basedir); |
diff --git a/webfinger.c b/webfinger.c index 2f11516..c7b73f7 100644 --- a/webfinger.c +++ b/webfinger.c | |||
| @@ -21,7 +21,7 @@ int webfinger_request(const char *qs, char **actor, char **user) | |||
| 21 | 21 | ||
| 22 | if (xs_startswith(qs, "https:/" "/")) { | 22 | if (xs_startswith(qs, "https:/" "/")) { |
| 23 | /* actor query: pick the host */ | 23 | /* actor query: pick the host */ |
| 24 | xs *s = xs_replace(qs, "https:/" "/", ""); | 24 | xs *s = xs_replace_n(qs, "https:/" "/", "", 1); |
| 25 | 25 | ||
| 26 | l = xs_split_n(s, "/", 1); | 26 | l = xs_split_n(s, "/", 1); |
| 27 | 27 | ||
| @@ -74,7 +74,7 @@ int webfinger_request(const char *qs, char **actor, char **user) | |||
| 74 | char *subject = xs_dict_get(obj, "subject"); | 74 | char *subject = xs_dict_get(obj, "subject"); |
| 75 | 75 | ||
| 76 | if (subject) | 76 | if (subject) |
| 77 | *user = xs_replace(subject, "acct:", ""); | 77 | *user = xs_replace_n(subject, "acct:", "", 1); |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | if (actor != NULL) { | 80 | if (actor != NULL) { |
| @@ -105,6 +105,8 @@ int webfinger_get_handler(d_char *req, char *q_path, | |||
| 105 | { | 105 | { |
| 106 | int status; | 106 | int status; |
| 107 | 107 | ||
| 108 | (void)b_size; | ||
| 109 | |||
| 108 | if (strcmp(q_path, "/.well-known/webfinger") != 0) | 110 | if (strcmp(q_path, "/.well-known/webfinger") != 0) |
| 109 | return 0; | 111 | return 0; |
| 110 | 112 | ||
| @@ -137,7 +139,7 @@ int webfinger_get_handler(d_char *req, char *q_path, | |||
| 137 | else | 139 | else |
| 138 | if (xs_startswith(resource, "acct:")) { | 140 | if (xs_startswith(resource, "acct:")) { |
| 139 | /* it's an account name */ | 141 | /* it's an account name */ |
| 140 | xs *an = xs_replace(resource, "acct:", ""); | 142 | xs *an = xs_replace_n(resource, "acct:", "", 1); |
| 141 | xs *l = NULL; | 143 | xs *l = NULL; |
| 142 | 144 | ||
| 143 | /* strip a possible leading @ */ | 145 | /* strip a possible leading @ */ |
| @@ -65,8 +65,10 @@ xs_str *xs_str_new(const char *str); | |||
| 65 | xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix); | 65 | xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix); |
| 66 | #define xs_str_prepend_i(str, prefix) xs_str_wrap_i(prefix, str, NULL) | 66 | #define xs_str_prepend_i(str, prefix) xs_str_wrap_i(prefix, str, NULL) |
| 67 | #define xs_str_cat(str, suffix) xs_str_wrap_i(NULL, str, suffix) | 67 | #define xs_str_cat(str, suffix) xs_str_wrap_i(NULL, str, suffix) |
| 68 | xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto); | 68 | xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times); |
| 69 | #define xs_replace(str, sfrom, sto) xs_replace_i(xs_dup(str), sfrom, sto) | 69 | #define xs_replace_i(str, sfrom, sto) xs_replace_in(str, sfrom, sto, XS_ALL) |
| 70 | #define xs_replace(str, sfrom, sto) xs_replace_in(xs_dup(str), sfrom, sto, XS_ALL) | ||
| 71 | #define xs_replace_n(str, sfrom, sto, times) xs_replace_in(xs_dup(str), sfrom, sto, times) | ||
| 70 | xs_str *xs_fmt(const char *fmt, ...); | 72 | xs_str *xs_fmt(const char *fmt, ...); |
| 71 | int xs_str_in(const char *haystack, const char *needle); | 73 | int xs_str_in(const char *haystack, const char *needle); |
| 72 | int _xs_startsorends(const char *str, const char *xfix, int ends); | 74 | int _xs_startsorends(const char *str, const char *xfix, int ends); |
| @@ -416,7 +418,7 @@ xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix) | |||
| 416 | } | 418 | } |
| 417 | 419 | ||
| 418 | 420 | ||
| 419 | xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto) | 421 | xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times) |
| 420 | /* replaces inline all sfrom with sto */ | 422 | /* replaces inline all sfrom with sto */ |
| 421 | { | 423 | { |
| 422 | XS_ASSERT_TYPE(str, XSTYPE_STRING); | 424 | XS_ASSERT_TYPE(str, XSTYPE_STRING); |
| @@ -426,7 +428,7 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto) | |||
| 426 | char *ss; | 428 | char *ss; |
| 427 | int offset = 0; | 429 | int offset = 0; |
| 428 | 430 | ||
| 429 | while ((ss = strstr(str + offset, sfrom)) != NULL) { | 431 | while (times > 0 && (ss = strstr(str + offset, sfrom)) != NULL) { |
| 430 | int n_offset = ss - str; | 432 | int n_offset = ss - str; |
| 431 | 433 | ||
| 432 | str = xs_collapse(str, n_offset, sfsz); | 434 | str = xs_collapse(str, n_offset, sfsz); |
| @@ -434,6 +436,8 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto) | |||
| 434 | memcpy(str + n_offset, sto, stsz); | 436 | memcpy(str + n_offset, sto, stsz); |
| 435 | 437 | ||
| 436 | offset = n_offset + stsz; | 438 | offset = n_offset + stsz; |
| 439 | |||
| 440 | times--; | ||
| 437 | } | 441 | } |
| 438 | 442 | ||
| 439 | return str; | 443 | return str; |
diff --git a/xs_encdec.h b/xs_encdec.h index 12f40ef..2502520 100644 --- a/xs_encdec.h +++ b/xs_encdec.h | |||
| @@ -7,13 +7,20 @@ | |||
| 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 | int xs_is_hex(const char *str); |
| 10 | xs_str *xs_base32_enc(const xs_val *data, int sz); | ||
| 11 | xs_str *xs_base32hex_enc(const xs_val *data, int sz); | ||
| 12 | xs_val *xs_base32_dec(const xs_str *data, int *size); | ||
| 13 | xs_val *xs_base32hex_dec(const xs_str *data, int *size); | ||
| 10 | xs_str *xs_base64_enc(const xs_val *data, int sz); | 14 | xs_str *xs_base64_enc(const xs_val *data, int sz); |
| 11 | xs_val *xs_base64_dec(const xs_str *data, int *size); | 15 | xs_val *xs_base64_dec(const xs_str *data, int *size); |
| 16 | int xs_is_base64(const char *str); | ||
| 12 | xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint); | 17 | xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint); |
| 13 | 18 | ||
| 14 | 19 | ||
| 15 | #ifdef XS_IMPLEMENTATION | 20 | #ifdef XS_IMPLEMENTATION |
| 16 | 21 | ||
| 22 | /** hex **/ | ||
| 23 | |||
| 17 | xs_str *xs_hex_enc(const xs_val *data, int size) | 24 | xs_str *xs_hex_enc(const xs_val *data, int size) |
| 18 | /* returns an hexdump of data */ | 25 | /* returns an hexdump of data */ |
| 19 | { | 26 | { |
| @@ -78,16 +85,178 @@ int xs_is_hex(const char *str) | |||
| 78 | } | 85 | } |
| 79 | 86 | ||
| 80 | 87 | ||
| 81 | xs_str *xs_base64_enc(const xs_val *data, int sz) | 88 | /** base32 */ |
| 82 | /* encodes data to base64 */ | 89 | |
| 90 | static char *xs_b32_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
| 91 | "234567="; | ||
| 92 | |||
| 93 | static char *xs_b32hex_tbl = "0123456789" | ||
| 94 | "ABCDEFGHIJKLMNOPQRSTUV="; | ||
| 95 | |||
| 96 | /* | ||
| 97 | 00000|00011|11111|12222|22223|33333|33444|44444 | ||
| 98 | */ | ||
| 99 | |||
| 100 | xs_str *xs_base32_enc_tbl(const xs_val *data, int sz, const char *b32_tbl) | ||
| 101 | /* encodes data to base32 using a table */ | ||
| 102 | { | ||
| 103 | xs_str *s = xs_str_new(NULL); | ||
| 104 | unsigned char *p; | ||
| 105 | int n; | ||
| 106 | |||
| 107 | p = (unsigned char *)data; | ||
| 108 | |||
| 109 | for (n = 0; n < sz; n += 5) { | ||
| 110 | int l = sz - n; | ||
| 111 | char enc[9] = "========"; | ||
| 112 | |||
| 113 | enc[0] = b32_tbl[(p[n] >> 3) & 0x1f]; | ||
| 114 | |||
| 115 | if (l > 1) { | ||
| 116 | enc[1] = b32_tbl[(p[n] << 2 | p[n + 1] >> 6) & 0x1f]; | ||
| 117 | enc[2] = b32_tbl[(p[n + 1] >> 1) & 0x1f]; | ||
| 118 | |||
| 119 | if (l > 2) { | ||
| 120 | enc[3] = b32_tbl[(p[n + 1] << 4 | p[n + 2] >> 4) & 0x1f]; | ||
| 121 | |||
| 122 | if (l > 3) { | ||
| 123 | enc[4] = b32_tbl[(p[n + 2] << 1 | p[n + 3] >> 7) & 0x1f]; | ||
| 124 | enc[5] = b32_tbl[(p[n + 3] >> 2) & 0x1f]; | ||
| 125 | |||
| 126 | if (l > 4) { | ||
| 127 | enc[6] = b32_tbl[(p[n + 3] << 3 | p[n + 4] >> 5) & 0x1f]; | ||
| 128 | enc[7] = b32_tbl[(p[n + 4]) & 0x1f]; | ||
| 129 | } | ||
| 130 | else | ||
| 131 | enc[6] = b32_tbl[(p[n + 3] << 3) & 0x1f]; | ||
| 132 | } | ||
| 133 | else | ||
| 134 | enc[4] = b32_tbl[(p[n + 2] << 1) & 0x1f]; | ||
| 135 | } | ||
| 136 | else | ||
| 137 | enc[3] = b32_tbl[(p[n + 1] << 4) & 0x1f]; | ||
| 138 | } | ||
| 139 | else | ||
| 140 | enc[1] = b32_tbl[(p[n] << 2) & 0x1f]; | ||
| 141 | |||
| 142 | s = xs_str_cat(s, enc); | ||
| 143 | } | ||
| 144 | |||
| 145 | return s; | ||
| 146 | } | ||
| 147 | |||
| 148 | |||
| 149 | xs_str *xs_base32_enc(const xs_val *data, int sz) | ||
| 150 | /* encodes data to base32 */ | ||
| 151 | { | ||
| 152 | return xs_base32_enc_tbl(data, sz, xs_b32_tbl); | ||
| 153 | } | ||
| 154 | |||
| 155 | |||
| 156 | xs_str *xs_base32hex_enc(const xs_val *data, int sz) | ||
| 157 | /* encodes data to base32 with HEX alphabet (RFC4648) */ | ||
| 158 | { | ||
| 159 | return xs_base32_enc_tbl(data, sz, xs_b32hex_tbl); | ||
| 160 | } | ||
| 161 | |||
| 162 | |||
| 163 | xs_val *xs_base32_dec_tbl(const xs_str *data, int *size, const char *b32_tbl) | ||
| 164 | /* decodes data from base32 using a table */ | ||
| 165 | { | ||
| 166 | xs_val *s = NULL; | ||
| 167 | int sz = 0; | ||
| 168 | char *p; | ||
| 169 | |||
| 170 | p = (char *)data; | ||
| 171 | |||
| 172 | /* size of data must be a multiple of 8 */ | ||
| 173 | if (strlen(p) % 8) | ||
| 174 | return NULL; | ||
| 175 | |||
| 176 | for (p = (char *)data; *p; p += 8) { | ||
| 177 | int cs[8]; | ||
| 178 | int n; | ||
| 179 | unsigned char tmp[5]; | ||
| 180 | |||
| 181 | for (n = 0; n < 8; n++) { | ||
| 182 | char *ss = strchr(b32_tbl, p[n]); | ||
| 183 | |||
| 184 | if (ss == NULL) { | ||
| 185 | /* not a base32 char */ | ||
| 186 | return xs_free(s); | ||
| 187 | } | ||
| 188 | |||
| 189 | cs[n] = ss - b32_tbl; | ||
| 190 | } | ||
| 191 | |||
| 192 | n = 0; | ||
| 193 | |||
| 194 | /* #0 byte */ | ||
| 195 | tmp[n++] = cs[0] << 3 | cs[1] >> 2; | ||
| 196 | |||
| 197 | if (cs[2] != 32) { | ||
| 198 | /* #1 byte */ | ||
| 199 | tmp[n++] = (cs[1] & 0x3) << 6 | cs[2] << 1 | (cs[3] & 0x10) >> 4; | ||
| 200 | |||
| 201 | if (cs[4] != 32) { | ||
| 202 | /* #2 byte */ | ||
| 203 | tmp[n++] = (cs[3] & 0xf) << 4 | cs[4] >> 1; | ||
| 204 | |||
| 205 | if (cs[5] != 32) { | ||
| 206 | /* #3 byte */ | ||
| 207 | tmp[n++] = (cs[4] & 0x1) << 7 | cs[5] << 2 | cs[6] >> 3; | ||
| 208 | |||
| 209 | if (cs[7] != 32) { | ||
| 210 | /* #4 byte */ | ||
| 211 | tmp[n++] = (cs[6] & 0x7) << 5 | cs[7]; | ||
| 212 | } | ||
| 213 | } | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | /* must be done manually because data can be pure binary */ | ||
| 218 | s = xs_realloc(s, _xs_blk_size(sz + n)); | ||
| 219 | memcpy(s + sz, tmp, n); | ||
| 220 | sz += n; | ||
| 221 | } | ||
| 222 | |||
| 223 | /* asciiz it to use it as a string */ | ||
| 224 | s = xs_realloc(s, _xs_blk_size(sz + 1)); | ||
| 225 | s[sz] = '\0'; | ||
| 226 | |||
| 227 | *size = sz; | ||
| 228 | |||
| 229 | return s; | ||
| 230 | } | ||
| 231 | |||
| 232 | |||
| 233 | xs_val *xs_base32_dec(const xs_str *data, int *size) | ||
| 234 | /* decodes data from base32 */ | ||
| 235 | { | ||
| 236 | return xs_base32_dec_tbl(data, size, xs_b32_tbl); | ||
| 237 | } | ||
| 238 | |||
| 239 | |||
| 240 | xs_val *xs_base32hex_dec(const xs_str *data, int *size) | ||
| 241 | /* decodes data from base32 with HEX alphabet (RFC4648) */ | ||
| 242 | { | ||
| 243 | return xs_base32_dec_tbl(data, size, xs_b32hex_tbl); | ||
| 244 | } | ||
| 245 | |||
| 246 | |||
| 247 | /** base64 */ | ||
| 248 | |||
| 249 | static char *xs_b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
| 250 | "abcdefghijklmnopqrstuvwxyz" | ||
| 251 | "0123456789+/="; | ||
| 252 | |||
| 253 | xs_str *xs_base64_enc_tbl(const xs_val *data, int sz, const char *b64_tbl) | ||
| 254 | /* encodes data to base64 using a table */ | ||
| 83 | { | 255 | { |
| 84 | xs_str *s; | 256 | xs_str *s; |
| 85 | unsigned char *p; | 257 | unsigned char *p; |
| 86 | char *i; | 258 | char *i; |
| 87 | int bsz, n; | 259 | int bsz, n; |
| 88 | static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
| 89 | "abcdefghijklmnopqrstuvwxyz" | ||
| 90 | "0123456789+/"; | ||
| 91 | 260 | ||
| 92 | bsz = ((sz + 3 - 1) / 3) * 4; | 261 | bsz = ((sz + 3 - 1) / 3) * 4; |
| 93 | i = s = xs_realloc(NULL, _xs_blk_size(bsz + 1)); | 262 | i = s = xs_realloc(NULL, _xs_blk_size(bsz + 1)); |
| @@ -123,15 +292,19 @@ xs_str *xs_base64_enc(const xs_val *data, int sz) | |||
| 123 | } | 292 | } |
| 124 | 293 | ||
| 125 | 294 | ||
| 126 | xs_val *xs_base64_dec(const xs_str *data, int *size) | 295 | xs_str *xs_base64_enc(const xs_val *data, int sz) |
| 127 | /* decodes data from base64 */ | 296 | /* encodes data to base64 */ |
| 297 | { | ||
| 298 | return xs_base64_enc_tbl(data, sz, xs_b64_tbl); | ||
| 299 | } | ||
| 300 | |||
| 301 | |||
| 302 | xs_val *xs_base64_dec_tbl(const xs_str *data, int *size, const char *b64_tbl) | ||
| 303 | /* decodes data from base64 using a table */ | ||
| 128 | { | 304 | { |
| 129 | xs_val *s = NULL; | 305 | xs_val *s = NULL; |
| 130 | int sz = 0; | 306 | int sz = 0; |
| 131 | char *p; | 307 | char *p; |
| 132 | static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
| 133 | "abcdefghijklmnopqrstuvwxyz" | ||
| 134 | "0123456789+/="; | ||
| 135 | 308 | ||
| 136 | p = (char *)data; | 309 | p = (char *)data; |
| 137 | 310 | ||
| @@ -184,6 +357,34 @@ xs_val *xs_base64_dec(const xs_str *data, int *size) | |||
| 184 | } | 357 | } |
| 185 | 358 | ||
| 186 | 359 | ||
| 360 | xs_val *xs_base64_dec(const xs_str *data, int *size) | ||
| 361 | /* decodes data from base64 */ | ||
| 362 | { | ||
| 363 | return xs_base64_dec_tbl(data, size, xs_b64_tbl); | ||
| 364 | } | ||
| 365 | |||
| 366 | |||
| 367 | int xs_is_base64_tbl(const char *str, const char *b64_tbl) | ||
| 368 | /* returns 1 if str is a base64 string, with table */ | ||
| 369 | { | ||
| 370 | while (*str) { | ||
| 371 | if (strchr(b64_tbl, *str++) == NULL) | ||
| 372 | return 0; | ||
| 373 | } | ||
| 374 | |||
| 375 | return 1; | ||
| 376 | } | ||
| 377 | |||
| 378 | |||
| 379 | int xs_is_base64(const char *str) | ||
| 380 | /* returns 1 if str is a base64 string */ | ||
| 381 | { | ||
| 382 | return xs_is_base64_tbl(str, xs_b64_tbl); | ||
| 383 | } | ||
| 384 | |||
| 385 | |||
| 386 | /** utf-8 **/ | ||
| 387 | |||
| 187 | xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint) | 388 | xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint) |
| 188 | /* encodes an Unicode codepoint to utf8 */ | 389 | /* encodes an Unicode codepoint to utf8 */ |
| 189 | { | 390 | { |
diff --git a/xs_version.h b/xs_version.h index 1d59f5e..6117ce9 100644 --- a/xs_version.h +++ b/xs_version.h | |||
| @@ -1 +1 @@ | |||
| /* a885c7cc4c8e6384ae23125ed05f434471ccc6fb */ | /* dfdd729248d7169b80cb6a7462fe6c0ba6efeb16 */ | ||