summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile7
-rw-r--r--Makefile.NetBSD8
-rw-r--r--README.md4
-rw-r--r--RELEASE_NOTES.md32
-rw-r--r--TODO.md12
-rw-r--r--activitypub.c146
-rw-r--r--data.c130
-rw-r--r--doc/snac.18
-rw-r--r--doc/snac.510
-rw-r--r--doc/snac.857
-rw-r--r--format.c79
-rw-r--r--html.c212
-rw-r--r--httpd.c3
-rw-r--r--main.c115
-rw-r--r--mastoapi.c177
-rw-r--r--snac.h14
-rw-r--r--xs.h103
-rw-r--r--xs_json.h107
-rw-r--r--xs_mime.h22
-rw-r--r--xs_unicode.h16
-rw-r--r--xs_url.h2
-rw-r--r--xs_version.h2
22 files changed, 954 insertions, 312 deletions
diff --git a/Makefile b/Makefile
index aa7ff87..39b44ef 100644
--- a/Makefile
+++ b/Makefile
@@ -36,15 +36,16 @@ uninstall:
36activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ 36activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
37 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h 37 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h
38data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ 38data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
39 xs_set.h xs_time.h snac.h 39 xs_set.h xs_time.h xs_regex.h snac.h
40format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h snac.h 40format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
41 xs_time.h snac.h
41html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ 42html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
42 xs_time.h xs_mime.h xs_match.h xs_html.h snac.h 43 xs_time.h xs_mime.h xs_match.h xs_html.h snac.h
43http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ 44http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
44 snac.h 45 snac.h
45httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_httpd.h xs_mime.h \ 46httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_httpd.h xs_mime.h \
46 xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h 47 xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h
47main.o: main.c xs.h xs_io.h xs_json.h xs_time.h snac.h xs_html.h 48main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h
48mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ 49mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
49 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ 50 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \
50 snac.h 51 snac.h
diff --git a/Makefile.NetBSD b/Makefile.NetBSD
index 5ab361f..67c77a5 100644
--- a/Makefile.NetBSD
+++ b/Makefile.NetBSD
@@ -1,6 +1,7 @@
1PREFIX=/usr/pkg 1PREFIX=/usr/pkg
2PREFIX_MAN=$(PREFIX)/man 2PREFIX_MAN=$(PREFIX)/man
3CFLAGS?=-g -Wall -Wextra 3CFLAGS?=-g -Wall -Wextra
4LDFLAGS=-lrt
4 5
5all: snac 6all: snac
6 7
@@ -37,15 +38,16 @@ uninstall:
37activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ 38activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
38 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h 39 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h
39data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ 40data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
40 xs_set.h xs_time.h snac.h 41 xs_set.h xs_time.h xs_regex.h snac.h
41format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h snac.h 42format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
43 xs_time.h snac.h
42html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ 44html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
43 xs_time.h xs_mime.h xs_match.h xs_html.h snac.h 45 xs_time.h xs_mime.h xs_match.h xs_html.h snac.h
44http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ 46http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
45 snac.h 47 snac.h
46httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_httpd.h xs_mime.h \ 48httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_httpd.h xs_mime.h \
47 xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h 49 xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h
48main.o: main.c xs.h xs_io.h xs_json.h xs_time.h snac.h xs_html.h 50main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h
49mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ 51mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
50 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ 52 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \
51 snac.h 53 snac.h
diff --git a/README.md b/README.md
index 5cc4aac..1e5c2e8 100644
--- a/README.md
+++ b/README.md
@@ -93,11 +93,13 @@ This will:
93- [Online snac manuals (user, administrator and data formats)](https://comam.es/snac-doc/). 93- [Online snac manuals (user, administrator and data formats)](https://comam.es/snac-doc/).
94- [How to run your own ActivityPub server on OpenBSD via snac (by Jordan Reger)](https://man.sr.ht/~jordanreger/activitypub-server-on-openbsd/). 94- [How to run your own ActivityPub server on OpenBSD via snac (by Jordan Reger)](https://man.sr.ht/~jordanreger/activitypub-server-on-openbsd/).
95- [How to install & run your own ActivityPub server on FreeBSD using snac, nginx, lets'encrypt (by gyptazy)](https://gyptazy.ch/blog/install-snac2-on-freebsd-an-activitypub-instance-for-the-fediverse/). 95- [How to install & run your own ActivityPub server on FreeBSD using snac, nginx, lets'encrypt (by gyptazy)](https://gyptazy.ch/blog/install-snac2-on-freebsd-an-activitypub-instance-for-the-fediverse/).
96- [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html).
97- [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac).
96 98
97## Incredibly awesome CSS themes for snac 99## Incredibly awesome CSS themes for snac
98 100
101- [A compilation of themes for snac (by Ворон)](https://codeberg.org/voron/snac-style).
99- [A cool, elegant theme (by Haijo7)](https://codeberg.org/Haijo7/snac-custom-css). 102- [A cool, elegant theme (by Haijo7)](https://codeberg.org/Haijo7/snac-custom-css).
100- [A light, lean theme (by Ворон)](https://codeberg.org/voron/snac-style).
101- [A terminal-like theme (by Tetra)](https://codeberg.org/ERROR404NULLNOTFOUND/snac-terminal-theme). 103- [A terminal-like theme (by Tetra)](https://codeberg.org/ERROR404NULLNOTFOUND/snac-terminal-theme).
102 104
103## License 105## License
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 6b52712..f272f5c 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,37 @@
1# Release Notes 1# Release Notes
2 2
3## 2.52
4
5Posts that were liked or boosted can now be unliked and unboosted.
6
7Added a header to avoid over-zealous caching in some browsers (contributed by louis77).
8
9## 2.51
10
11Support for custom Emojis has been added; they are no longer hardcoded, but read from the `emojis.json` file at the server base directory. Also, they are no longer limited to string substitutions, but images as external URLs are also supported (see `snac(8)` for more information).
12
13Fixed a bug that caused some notifications to be lost when coming from a user in the same instance.
14
15Added an additional check for blocked instances (sometimes, posts from blocked sites that were ancestors of legit posts were 'leaking' into the timeline).
16
17On OpenBSD, if the `disable_email_notifications` server flag is set to `true`, `unveil()` is not called for the execution of the `/usr/sbin/sendmail` binary and `pledge()` doesn't set the `exec` promise.
18
19## 2.50
20
21Incoming posts can now be filtered out by content using regular expressions on a server level (these regexes are written in the `filter_reject.txt` file at the server base directory; see `snac(5)` and `snac(8)`).
22
23Improved page position after hitting the `Hide` or `MUTE` buttons (for most cases).
24
25Use a shorter maximum conversation thread level (also, this maximum value is now configurable at compilation level with the `MAX_CONVERSATION_LEVELS` define).
26
27Fixed a bug where editing a post made the attached media or video to be lost.
28
29The way of refreshing remote actor data has been improved.
30
31Posting from the command-line now allows attachments.
32
33Added defines for time to enable MacOS builds (contributed by andypiper).
34
3## 2.49 35## 2.49
4 36
5Mastodon API: Fixed a bug in how validated links are reported. 37Mastodon API: Fixed a bug in how validated links are reported.
diff --git a/TODO.md b/TODO.md
index c4d6661..bdb4f23 100644
--- a/TODO.md
+++ b/TODO.md
@@ -6,19 +6,19 @@ Unfollowing lemmy groups gets rejected with an http status of 400.
6 6
7Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't. 7Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't.
8 8
9Post edits should preserve the image and the image description somewhat.
10
11Mastodon API: fix whatever the fuck is making the official app and Megalodon to crash. 9Mastodon API: fix whatever the fuck is making the official app and Megalodon to crash.
12 10
13Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721 11Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721
14 12
15## Wishlist 13## Wishlist
16 14
15Implement `Group`-like accounts (i.e. an actor that boosts to their followers all posts that mention it).
16
17Integrate "Ability to federate with hidden networks" see https://codeberg.org/grunfink/snac2/issues/93 17Integrate "Ability to federate with hidden networks" see https://codeberg.org/grunfink/snac2/issues/93
18 18
19Integrate "Added handling for International Domain Names" PR https://codeberg.org/grunfink/snac2/pulls/104 19Integrate "Added handling for International Domain Names" PR https://codeberg.org/grunfink/snac2/pulls/104
20 20
21Consider discarding posts by content using string or regex to mitigate spam. 21Consider adding Mastodon import functionality (for following_accounts.csv and outbox.json).
22 22
23Consider adding milter-like support to reject posts to mitigate spam. 23Consider adding milter-like support to reject posts to mitigate spam.
24 24
@@ -26,7 +26,7 @@ Do something about Akkoma and Misskey's quoted replies (they use the `quoteUrl`
26 26
27Add more CSS classes according to https://comam.es/snac/grunfink/p/1705598619.090050 27Add more CSS classes according to https://comam.es/snac/grunfink/p/1705598619.090050
28 28
29Add support for /share?text=tt&website=url (whatever it is). 29Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details).
30 30
31Add support for /authorize_interaction (whatever it is). 31Add support for /authorize_interaction (whatever it is).
32 32
@@ -307,3 +307,7 @@ Add support for rel="me" links, see https://codeberg.org/grunfink/snac2/issues/1
307Hide followers-only replies to unknown accounts, see https://codeberg.org/grunfink/snac2/issues/123 (2024-02-22T12:40:58+0100). 307Hide followers-only replies to unknown accounts, see https://codeberg.org/grunfink/snac2/issues/123 (2024-02-22T12:40:58+0100).
308 308
309Consider implementing the rejection of activities from recently-created accounts to mitigate spam, see https://akkoma.dev/AkkomaGang/akkoma/src/branch/develop/lib/pleroma/web/activity_pub/mrf/reject_newly_created_account_note_policy.ex (2024-02-24T07:46:10+0100). 309Consider implementing the rejection of activities from recently-created accounts to mitigate spam, see https://akkoma.dev/AkkomaGang/akkoma/src/branch/develop/lib/pleroma/web/activity_pub/mrf/reject_newly_created_account_note_policy.ex (2024-02-24T07:46:10+0100).
310
311Consider discarding posts by content using string or regex to mitigate spam (2024-03-14T10:40:14+0100).
312
313Post edits should preserve the image and the image description somewhat (2024-03-22T09:57:18+0100).
diff --git a/activitypub.c b/activitypub.c
index 73fbbc6..53f102e 100644
--- a/activitypub.c
+++ b/activitypub.c
@@ -125,10 +125,10 @@ int actor_request(snac *user, const char *actor, xs_dict **data)
125 *data = NULL; 125 *data = NULL;
126 126
127 /* get from disk first */ 127 /* get from disk first */
128 status = actor_get(actor, data); 128 status = actor_get_refresh(user, actor, data);
129 129
130 if (status != 200) { 130 if (!valid_status(status)) {
131 /* actor data non-existent or stale: get from the net */ 131 /* actor data non-existent: get from the net */
132 status = activitypub_request(user, actor, &payload); 132 status = activitypub_request(user, actor, &payload);
133 133
134 if (valid_status(status)) { 134 if (valid_status(status)) {
@@ -149,8 +149,6 @@ int actor_request(snac *user, const char *actor, xs_dict **data)
149 if (valid_status(status) && data && *data) 149 if (valid_status(status) && data && *data)
150 inbox_add_by_actor(*data); 150 inbox_add_by_actor(*data);
151 } 151 }
152 else
153 srv_debug(2, xs_fmt("NOT collected"));
154 152
155 return status; 153 return status;
156} 154}
@@ -313,6 +311,12 @@ int timeline_request(snac *snac, char **id, xs_str **wrk, int level)
313 if (level < MAX_CONVERSATION_LEVELS && !xs_is_null(*id)) { 311 if (level < MAX_CONVERSATION_LEVELS && !xs_is_null(*id)) {
314 xs *msg = NULL; 312 xs *msg = NULL;
315 313
314 /* from a blocked instance? discard and break */
315 if (is_instance_blocked(*id)) {
316 snac_debug(snac, 1, xs_fmt("timeline_request blocked instance %s", *id));
317 return status;
318 }
319
316 /* is the object already there? */ 320 /* is the object already there? */
317 if (!valid_status(object_get(*id, &msg))) { 321 if (!valid_status(object_get(*id, &msg))) {
318 /* no; download it */ 322 /* no; download it */
@@ -354,18 +358,22 @@ int timeline_request(snac *snac, char **id, xs_str **wrk, int level)
354 if (xs_match(type, "Note|Page|Article|Video")) { 358 if (xs_match(type, "Note|Page|Article|Video")) {
355 const char *actor = get_atto(object); 359 const char *actor = get_atto(object);
356 360
357 /* request (and drop) the actor for this entry */ 361 if (content_check("filter_reject.txt", object))
358 if (!xs_is_null(actor)) 362 snac_log(snac, xs_fmt("timeline_request rejected by content %s", nid));
359 actor_request(snac, actor, NULL); 363 else {
364 /* request (and drop) the actor for this entry */
365 if (!xs_is_null(actor))
366 actor_request(snac, actor, NULL);
360 367
361 /* does it have an ancestor? */ 368 /* does it have an ancestor? */
362 char *in_reply_to = xs_dict_get(object, "inReplyTo"); 369 char *in_reply_to = xs_dict_get(object, "inReplyTo");
363 370
364 /* store */ 371 /* store */
365 timeline_add(snac, nid, object); 372 timeline_add(snac, nid, object);
366 373
367 /* recurse! */ 374 /* recurse! */
368 timeline_request(snac, &in_reply_to, NULL, level + 1); 375 timeline_request(snac, &in_reply_to, NULL, level + 1);
376 }
369 } 377 }
370 } 378 }
371 } 379 }
@@ -623,6 +631,12 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
623 const char *type = xs_dict_get(c_msg, "type"); 631 const char *type = xs_dict_get(c_msg, "type");
624 const char *actor = xs_dict_get(c_msg, "actor"); 632 const char *actor = xs_dict_get(c_msg, "actor");
625 633
634 if (strcmp(actor, snac->actor) == 0) {
635 /* message by myself? (most probably via the shared-inbox) reject */
636 snac_debug(snac, 1, xs_fmt("ignoring message by myself"));
637 return 0;
638 }
639
626 if (xs_match(type, "Like|Announce")) { 640 if (xs_match(type, "Like|Announce")) {
627 const char *object = xs_dict_get(c_msg, "object"); 641 const char *object = xs_dict_get(c_msg, "object");
628 642
@@ -657,6 +671,12 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
657 return !xs_is_null(object) && strcmp(snac->actor, object) == 0; 671 return !xs_is_null(object) && strcmp(snac->actor, object) == 0;
658 } 672 }
659 673
674 /* only accept Ping directed to us */
675 if (xs_match(type, "Ping")) {
676 char *dest = xs_dict_get(c_msg, "to");
677 return !xs_is_null(dest) && strcmp(snac->actor, dest) == 0;
678 }
679
660 /* if it's not a Create or Update, allow as is */ 680 /* if it's not a Create or Update, allow as is */
661 if (!xs_match(type, "Create|Update")) { 681 if (!xs_match(type, "Create|Update")) {
662 return 1; 682 return 1;
@@ -1072,7 +1092,7 @@ xs_dict *msg_collection(snac *snac, char *id)
1072 1092
1073 msg = xs_dict_append(msg, "attributedTo", snac->actor); 1093 msg = xs_dict_append(msg, "attributedTo", snac->actor);
1074 msg = xs_dict_append(msg, "orderedItems", ol); 1094 msg = xs_dict_append(msg, "orderedItems", ol);
1075 msg = xs_dict_append(msg, "totalItems", xs_stock_0); 1095 msg = xs_dict_append(msg, "totalItems", xs_stock(0));
1076 1096
1077 return msg; 1097 return msg;
1078} 1098}
@@ -1129,8 +1149,10 @@ xs_dict *msg_admiration(snac *snac, char *object, char *type)
1129 1149
1130 if (valid_status(object_get(object, &a_msg))) { 1150 if (valid_status(object_get(object, &a_msg))) {
1131 xs *rcpts = xs_list_new(); 1151 xs *rcpts = xs_list_new();
1152 xs *o_md5 = xs_md5_hex(object, strlen(object));
1153 xs *id = xs_fmt("%s/%s/%s", snac->actor, *type == 'L' ? "l" : "a", o_md5);
1132 1154
1133 msg = msg_base(snac, type, "@dummy", snac->actor, "@now", object); 1155 msg = msg_base(snac, type, id, snac->actor, "@now", object);
1134 1156
1135 if (is_msg_public(a_msg)) 1157 if (is_msg_public(a_msg))
1136 rcpts = xs_list_append(rcpts, public_address); 1158 rcpts = xs_list_append(rcpts, public_address);
@@ -1146,6 +1168,33 @@ xs_dict *msg_admiration(snac *snac, char *object, char *type)
1146} 1168}
1147 1169
1148 1170
1171xs_dict *msg_repulsion(snac *user, char *id, char *type)
1172/* creates an Undo + admiration message */
1173{
1174 xs *a_msg = NULL;
1175 xs_dict *msg = NULL;
1176
1177 if (valid_status(object_get(id, &a_msg))) {
1178 /* create a clone of the original admiration message */
1179 xs *object = msg_admiration(user, id, type);
1180
1181 /* delete the published date */
1182 object = xs_dict_del(object, "published");
1183
1184 /* create an undo message for this object */
1185 msg = msg_undo(user, object);
1186
1187 /* copy the 'to' field */
1188 msg = xs_dict_set(msg, "to", xs_dict_get(object, "to"));
1189 }
1190
1191 /* now we despise this */
1192 object_unadmire(id, user->actor, *type == 'L' ? 1 : 0);
1193
1194 return msg;
1195}
1196
1197
1149xs_dict *msg_actor(snac *snac) 1198xs_dict *msg_actor(snac *snac)
1150/* create a Person message for this actor */ 1199/* create a Person message for this actor */
1151{ 1200{
@@ -1170,7 +1219,7 @@ xs_dict *msg_actor(snac *snac)
1170 msg = xs_dict_set(msg, "preferredUsername", snac->uid); 1219 msg = xs_dict_set(msg, "preferredUsername", snac->uid);
1171 msg = xs_dict_set(msg, "published", xs_dict_get(snac->config, "published")); 1220 msg = xs_dict_set(msg, "published", xs_dict_get(snac->config, "published"));
1172 1221
1173 xs *f_bio_2 = not_really_markdown(xs_dict_get(snac->config, "bio"), NULL); 1222 xs *f_bio_2 = not_really_markdown(xs_dict_get(snac->config, "bio"), NULL, NULL);
1174 f_bio = process_tags(snac, f_bio_2, &tags); 1223 f_bio = process_tags(snac, f_bio_2, &tags);
1175 msg = xs_dict_set(msg, "summary", f_bio); 1224 msg = xs_dict_set(msg, "summary", f_bio);
1176 msg = xs_dict_set(msg, "tag", tags); 1225 msg = xs_dict_set(msg, "tag", tags);
@@ -1378,7 +1427,7 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
1378 } 1427 }
1379 1428
1380 /* format the content */ 1429 /* format the content */
1381 fc2 = not_really_markdown(content, &atls); 1430 fc2 = not_really_markdown(content, &atls, &tag);
1382 1431
1383 if (in_reply_to != NULL && *in_reply_to) { 1432 if (in_reply_to != NULL && *in_reply_to) {
1384 xs *p_msg = NULL; 1433 xs *p_msg = NULL;
@@ -1556,6 +1605,7 @@ xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
1556 } 1605 }
1557 1606
1558 if (xs_set_add(&seen, v2) == 1) { 1607 if (xs_set_add(&seen, v2) == 1) {
1608 d = xs_dict_append(d, "type", "Note");
1559 d = xs_dict_append(d, "name", v2); 1609 d = xs_dict_append(d, "name", v2);
1560 d = xs_dict_append(d, "replies", replies); 1610 d = xs_dict_append(d, "replies", replies);
1561 o = xs_list_append(o, d); 1611 o = xs_list_append(o, d);
@@ -1608,7 +1658,7 @@ int update_question(snac *user, const char *id)
1608 const char *name = xs_dict_get(v, "name"); 1658 const char *name = xs_dict_get(v, "name");
1609 if (name) { 1659 if (name) {
1610 lopts = xs_list_append(lopts, name); 1660 lopts = xs_list_append(lopts, name);
1611 rcnt = xs_dict_set(rcnt, name, xs_stock_0); 1661 rcnt = xs_dict_set(rcnt, name, xs_stock(0));
1612 } 1662 }
1613 } 1663 }
1614 1664
@@ -1891,6 +1941,8 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
1891 } 1941 }
1892 else 1942 else
1893 if (strcmp(type, "Undo") == 0) { /** **/ 1943 if (strcmp(type, "Undo") == 0) { /** **/
1944 char *id = xs_dict_get(object, "object");
1945
1894 if (xs_type(object) != XSTYPE_DICT) 1946 if (xs_type(object) != XSTYPE_DICT)
1895 utype = "Follow"; 1947 utype = "Follow";
1896 1948
@@ -1903,6 +1955,23 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
1903 snac_log(snac, xs_fmt("error deleting follower %s", actor)); 1955 snac_log(snac, xs_fmt("error deleting follower %s", actor));
1904 } 1956 }
1905 else 1957 else
1958 if (strcmp(utype, "Like") == 0) { /** **/
1959 int status = object_unadmire(id, actor, 1);
1960
1961 snac_log(snac, xs_fmt("Unlike for %s %d", id, status));
1962 }
1963 else
1964 if (strcmp(utype, "Announce") == 0) { /** **/
1965 int status = 200;
1966
1967 /* commented out: if a followed user boosts something that
1968 is requested and then unboosts, the post remains here,
1969 but with no apparent reason, and that is confusing */
1970 //status = object_unadmire(id, actor, 0);
1971
1972 snac_log(snac, xs_fmt("Unboost for %s %d", id, status));
1973 }
1974 else
1906 snac_debug(snac, 1, xs_fmt("ignored 'Undo' for object type '%s'", utype)); 1975 snac_debug(snac, 1, xs_fmt("ignored 'Undo' for object type '%s'", utype));
1907 } 1976 }
1908 else 1977 else
@@ -1912,7 +1981,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
1912 return 1; 1981 return 1;
1913 } 1982 }
1914 1983
1915 if (strcmp(utype, "Note") == 0) { /** **/ 1984 if (xs_match(utype, "Note|Article")) { /** **/
1916 char *id = xs_dict_get(object, "id"); 1985 char *id = xs_dict_get(object, "id");
1917 char *in_reply_to = xs_dict_get(object, "inReplyTo"); 1986 char *in_reply_to = xs_dict_get(object, "inReplyTo");
1918 xs *wrk = NULL; 1987 xs *wrk = NULL;
@@ -1921,10 +1990,15 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
1921 snac_debug(snac, 0, xs_fmt("dropped reply %s to hidden post %s", id, in_reply_to)); 1990 snac_debug(snac, 0, xs_fmt("dropped reply %s to hidden post %s", id, in_reply_to));
1922 } 1991 }
1923 else { 1992 else {
1993 if (content_check("filter_reject.txt", object)) {
1994 snac_log(snac, xs_fmt("rejected by content %s", id));
1995 return 1;
1996 }
1997
1924 timeline_request(snac, &in_reply_to, &wrk, 0); 1998 timeline_request(snac, &in_reply_to, &wrk, 0);
1925 1999
1926 if (timeline_add(snac, id, object)) { 2000 if (timeline_add(snac, id, object)) {
1927 snac_log(snac, xs_fmt("new 'Note' %s %s", actor, id)); 2001 snac_log(snac, xs_fmt("new '%s' %s %s", utype, actor, id));
1928 do_notify = 1; 2002 do_notify = 1;
1929 } 2003 }
1930 2004
@@ -1988,12 +2062,12 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
1988 if (xs_type(object) == XSTYPE_DICT) 2062 if (xs_type(object) == XSTYPE_DICT)
1989 object = xs_dict_get(object, "id"); 2063 object = xs_dict_get(object, "id");
1990 2064
1991 if (timeline_admire(snac, object, actor, 1) == 201) { 2065 if (timeline_admire(snac, object, actor, 1) == 201)
1992 snac_log(snac, xs_fmt("new 'Like' %s %s", actor, object)); 2066 snac_log(snac, xs_fmt("new 'Like' %s %s", actor, object));
1993 do_notify = 1;
1994 }
1995 else 2067 else
1996 snac_log(snac, xs_fmt("repeated 'Like' from %s to %s", actor, object)); 2068 snac_log(snac, xs_fmt("repeated 'Like' from %s to %s", actor, object));
2069
2070 do_notify = 1;
1997 } 2071 }
1998 else 2072 else
1999 if (strcmp(type, "Announce") == 0) { /** **/ 2073 if (strcmp(type, "Announce") == 0) { /** **/
@@ -2019,13 +2093,13 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
2019 xs *who_o = NULL; 2093 xs *who_o = NULL;
2020 2094
2021 if (valid_status(actor_request(snac, who, &who_o))) { 2095 if (valid_status(actor_request(snac, who, &who_o))) {
2022 if (timeline_admire(snac, object, actor, 0) == 201) { 2096 if (timeline_admire(snac, object, actor, 0) == 201)
2023 snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object)); 2097 snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
2024 do_notify = 1;
2025 }
2026 else 2098 else
2027 snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s", 2099 snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
2028 actor, object)); 2100 actor, object));
2101
2102 do_notify = 1;
2029 } 2103 }
2030 else 2104 else
2031 snac_debug(snac, 1, xs_fmt("dropped 'Announce' on actor request error %s", who)); 2105 snac_debug(snac, 1, xs_fmt("dropped 'Announce' on actor request error %s", who));
@@ -2236,6 +2310,24 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
2236 verify_links(snac); 2310 verify_links(snac);
2237 } 2311 }
2238 else 2312 else
2313 if (strcmp(type, "actor_refresh") == 0) {
2314 const char *actor = xs_dict_get(q_item, "actor");
2315 double mtime = object_mtime(actor);
2316
2317 /* only refresh if it was refreshed more than an hour ago */
2318 if (mtime + 3600.0 < (double) time(NULL)) {
2319 xs *actor_o = NULL;
2320 int status;
2321
2322 if (valid_status((status = activitypub_request(snac, actor, &actor_o))))
2323 actor_add(actor, actor_o);
2324 else
2325 object_touch(actor);
2326
2327 snac_log(snac, xs_fmt("actor_refresh %s %d", actor, status));
2328 }
2329 }
2330 else
2239 snac_log(snac, xs_fmt("unexpected user q_item type '%s'", type)); 2331 snac_log(snac, xs_fmt("unexpected user q_item type '%s'", type));
2240} 2332}
2241 2333
diff --git a/data.c b/data.c
index f4cd6d6..d3045f4 100644
--- a/data.c
+++ b/data.c
@@ -9,6 +9,7 @@
9#include "xs_glob.h" 9#include "xs_glob.h"
10#include "xs_set.h" 10#include "xs_set.h"
11#include "xs_time.h" 11#include "xs_time.h"
12#include "xs_regex.h"
12 13
13#include "snac.h" 14#include "snac.h"
14 15
@@ -116,21 +117,33 @@ int srv_open(char *basedir, int auto_upgrade)
116 srv_debug(1, xs_dup("OpenBSD security disabled by admin")); 117 srv_debug(1, xs_dup("OpenBSD security disabled by admin"));
117 } 118 }
118 else { 119 else {
120 int smail = xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE;
121
119 srv_debug(1, xs_fmt("Calling unveil()")); 122 srv_debug(1, xs_fmt("Calling unveil()"));
120 unveil(basedir, "rwc"); 123 unveil(basedir, "rwc");
121 unveil("/tmp", "rwc"); 124 unveil("/tmp", "rwc");
122 unveil("/usr/sbin/sendmail", "x");
123 unveil("/etc/resolv.conf", "r"); 125 unveil("/etc/resolv.conf", "r");
124 unveil("/etc/hosts", "r"); 126 unveil("/etc/hosts", "r");
125 unveil("/etc/ssl/openssl.cnf", "r"); 127 unveil("/etc/ssl/openssl.cnf", "r");
126 unveil("/etc/ssl/cert.pem", "r"); 128 unveil("/etc/ssl/cert.pem", "r");
127 unveil("/usr/share/zoneinfo", "r"); 129 unveil("/usr/share/zoneinfo", "r");
130
131 if (smail)
132 unveil("/usr/sbin/sendmail", "x");
133
128 unveil(NULL, NULL); 134 unveil(NULL, NULL);
129 srv_debug(1, xs_fmt("Calling pledge()")); 135 srv_debug(1, xs_fmt("Calling pledge()"));
130 pledge("stdio rpath wpath cpath flock inet proc exec dns fattr", NULL); 136
137 if (smail)
138 pledge("stdio rpath wpath cpath flock inet proc exec dns fattr", NULL);
139 else
140 pledge("stdio rpath wpath cpath flock inet proc dns fattr", NULL);
131 } 141 }
132#endif /* __OpenBSD__ */ 142#endif /* __OpenBSD__ */
133 143
144 /* read (and drop) emojis.json, possibly creating it */
145 xs_free(emojis());
146
134 return ret; 147 return ret;
135} 148}
136 149
@@ -393,7 +406,7 @@ int index_del_md5(const char *fn, const char *md5)
393 fclose(f); 406 fclose(f);
394 } 407 }
395 else 408 else
396 status = 500; 409 status = 410;
397 410
398 pthread_mutex_unlock(&data_mutex); 411 pthread_mutex_unlock(&data_mutex);
399 412
@@ -796,6 +809,30 @@ double object_ctime(const char *id)
796} 809}
797 810
798 811
812double object_mtime_by_md5(const char *md5)
813{
814 xs *fn = _object_fn_by_md5(md5, "object_mtime_by_md5");
815 return mtime(fn);
816}
817
818
819double object_mtime(const char *id)
820{
821 xs *md5 = xs_md5_hex(id, strlen(id));
822 return object_mtime_by_md5(md5);
823}
824
825
826void object_touch(const char *id)
827{
828 xs *md5 = xs_md5_hex(id, strlen(id));
829 xs *fn = _object_fn_by_md5(md5, "object_touch");
830
831 if (mtime(fn))
832 utimes(fn, NULL);
833}
834
835
799xs_str *_object_index_fn(const char *id, const char *idxsfx) 836xs_str *_object_index_fn(const char *id, const char *idxsfx)
800/* returns the filename of an object's index */ 837/* returns the filename of an object's index */
801{ 838{
@@ -880,6 +917,9 @@ int object_unadmire(const char *id, const char *actor, int like)
880 917
881 status = index_del(fn, actor); 918 status = index_del(fn, actor);
882 919
920 if (valid_status(status))
921 index_gc(fn);
922
883 srv_debug(0, 923 srv_debug(0,
884 xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status)); 924 xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
885 925
@@ -1551,7 +1591,6 @@ int actor_get(const char *actor, xs_dict **data)
1551 else 1591 else
1552 d = xs_free(d); 1592 d = xs_free(d);
1553 1593
1554#ifdef STALE_ACTORS
1555 xs *fn = _object_fn(actor); 1594 xs *fn = _object_fn(actor);
1556 double max_time; 1595 double max_time;
1557 1596
@@ -1560,13 +1599,20 @@ int actor_get(const char *actor, xs_dict **data)
1560 1599
1561 if (mtime(fn) + max_time < (double) time(NULL)) { 1600 if (mtime(fn) + max_time < (double) time(NULL)) {
1562 /* actor data exists but also stinks */ 1601 /* actor data exists but also stinks */
1563
1564 /* touch the file */
1565 utimes(fn, NULL);
1566
1567 status = 205; /* "205: Reset Content" "110: Response Is Stale" */ 1602 status = 205; /* "205: Reset Content" "110: Response Is Stale" */
1568 } 1603 }
1569#endif /* STALE_ACTORS */ 1604
1605 return status;
1606}
1607
1608
1609int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
1610/* gets an actor and requests a refresh if it's stale */
1611{
1612 int status = actor_get(actor, data);
1613
1614 if (status == 205 && user && !xs_startswith(actor, srv_baseurl))
1615 enqueue_actor_refresh(user, actor);
1570 1616
1571 return status; 1617 return status;
1572} 1618}
@@ -2007,6 +2053,47 @@ int instance_unblock(const char *instance)
2007} 2053}
2008 2054
2009 2055
2056/** content filtering **/
2057
2058int content_check(const char *file, const xs_dict *msg)
2059/* checks if a message's content matches any of the regexes in file */
2060/* file format: one regex per line */
2061{
2062 xs *fn = xs_fmt("%s/%s", srv_basedir, file);
2063 FILE *f;
2064 int r = 0;
2065 char *v = xs_dict_get(msg, "content");
2066
2067 if (xs_type(v) == XSTYPE_STRING && *v) {
2068 if ((f = fopen(fn, "r")) != NULL) {
2069 srv_debug(1, xs_fmt("content_check: loading regexes from %s", fn));
2070
2071 /* massage content (strip HTML tags, etc.) */
2072 xs *c = xs_regex_replace(v, "<[^>]+>", " ");
2073 c = xs_regex_replace_i(c, " {2,}", " ");
2074 c = xs_tolower_i(c);
2075
2076 while (!r && !feof(f)) {
2077 xs *rx = xs_strip_i(xs_readline(f));
2078
2079 if (*rx) {
2080 xs *l = xs_regex_select_n(c, rx, 1);
2081
2082 if (xs_list_len(l)) {
2083 srv_debug(1, xs_fmt("content_check: match for '%s'", rx));
2084 r = 1;
2085 }
2086 }
2087 }
2088
2089 fclose(f);
2090 }
2091 }
2092
2093 return r;
2094}
2095
2096
2010/** notifications **/ 2097/** notifications **/
2011 2098
2012xs_str *notify_check_time(snac *snac, int reset) 2099xs_str *notify_check_time(snac *snac, int reset)
@@ -2388,6 +2475,21 @@ void enqueue_verify_links(snac *user)
2388} 2475}
2389 2476
2390 2477
2478void enqueue_actor_refresh(snac *user, const char *actor)
2479/* enqueues an actor refresh */
2480{
2481 xs *qmsg = _new_qmsg("actor_refresh", "", 0);
2482 char *ntid = xs_dict_get(qmsg, "ntid");
2483 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
2484
2485 qmsg = xs_dict_append(qmsg, "actor", actor);
2486
2487 qmsg = _enqueue_put(fn, qmsg);
2488
2489 snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
2490}
2491
2492
2391void enqueue_request_replies(snac *user, const char *id) 2493void enqueue_request_replies(snac *user, const char *id)
2392/* enqueues a request for the replies of a message */ 2494/* enqueues a request for the replies of a message */
2393{ 2495{
@@ -2645,6 +2747,16 @@ void purge_server(void)
2645 } 2747 }
2646 } 2748 }
2647 } 2749 }
2750
2751 /* delete index backups */
2752 xs *specb = xs_fmt("%s/" "*.bak", v);
2753 xs *bakfs = xs_glob(specb, 0, 0);
2754
2755 p2 = bakfs;
2756 while (xs_list_iter(&p2, &v2)) {
2757 unlink(v2);
2758 srv_debug(1, xs_fmt("purged %s", v2));
2759 }
2648 } 2760 }
2649 } 2761 }
2650 2762
diff --git a/doc/snac.1 b/doc/snac.1
index c3a84a9..178e594 100644
--- a/doc/snac.1
+++ b/doc/snac.1
@@ -221,12 +221,16 @@ Sends a Follow message for the specified actor URL.
221.It Cm request Ar basedir Ar uid Ar url 221.It Cm request Ar basedir Ar uid Ar url
222Requests an object and dumps it to stdout. This is a very low 222Requests an object and dumps it to stdout. This is a very low
223level command that is not very useful to you. 223level command that is not very useful to you.
224.It Cm note Ar basedir Ar uid Ar text 224.It Cm announce Ar basedir Ar uid Ar url
225Announces (boosts) a post via its URL.
226.It Cm note Ar basedir Ar uid Ar text Op file file ...
225Enqueues a Create + Note message to all followers. If the 227Enqueues a Create + Note message to all followers. If the
226.Ar text 228.Ar text
227argument is -e, the external editor defined by the EDITOR 229argument is -e, the external editor defined by the EDITOR
228environment variable will be invoked to prepare a message; if 230environment variable will be invoked to prepare a message; if
229it's - (a lonely hyphen), the post content will be read from stdin. 231it's - (a lonely hyphen), the post content will be read from stdin.
232The rest of command line arguments are treated as media files to be
233attached to the post.
230.It Cm block Ar basedir Ar instance_url 234.It Cm block Ar basedir Ar instance_url
231Blocks a full instance, given its URL or domain name. All subsequent 235Blocks a full instance, given its URL or domain name. All subsequent
232incoming activities with identifiers from that instance will be immediately 236incoming activities with identifiers from that instance will be immediately
diff --git a/doc/snac.5 b/doc/snac.5
index f10a77b..c460c7b 100644
--- a/doc/snac.5
+++ b/doc/snac.5
@@ -46,6 +46,9 @@ Strings in the format @user@host are requested using the Webfinger
46protocol and converted to links and mentions if something reasonable 46protocol and converted to links and mentions if something reasonable
47is found. 47is found.
48.It Emoticons / Smileys / Silly Symbols 48.It Emoticons / Smileys / Silly Symbols
49(Note: from version 2.51, these symbols are configurable by the
50instance administrator, so the available ones may differ).
51.Pp
49The following traditional ASCII emoticons or special strings are 52The following traditional ASCII emoticons or special strings are
50converted to related emojis: 53converted to related emojis:
51.Bd -literal 54.Bd -literal
@@ -106,6 +109,13 @@ This file is served when the server base URL is requested from a web browser. Se
106for more information about the customization options. 109for more information about the customization options.
107.It Pa public.idx 110.It Pa public.idx
108This file contains the list of public posts from all users in the server. 111This file contains the list of public posts from all users in the server.
112.It Pa filter_reject.txt
113This (optional) file contains a list of regular expressions, one per line, to be
114applied to the content of all incoming posts; if any of them match, the post is
115rejected. This brings the flexibility and destruction power of regular expressions
116to your Fediverse experience. To be used wisely (see
117.Xr snac 8
118for more information).
109.El 119.El
110.Pp 120.Pp
111Each user directory is a subdirectory of 121Each user directory is a subdirectory of
diff --git a/doc/snac.8 b/doc/snac.8
index 85106d3..4929a52 100644
--- a/doc/snac.8
+++ b/doc/snac.8
@@ -230,9 +230,56 @@ for details. Further, every user can have a private CSS file in their
230that will be served instead of the server-wide one. 230that will be served instead of the server-wide one.
231It's not modifiable from the web interface to avoid users 231It's not modifiable from the web interface to avoid users
232shooting themselves in the foot by destroying everything. 232shooting themselves in the foot by destroying everything.
233.Ss Old Data Purging 233.Ss Custom Emojis
234From version 2.06, there is no longer a need to add a special 234From version 2.51, support for customized Emojis in posts is available
235cron job for purging old data, as this is managed internally. 235(previously, they were hardcoded). Emojis are read from the
236.Pa emojis.json
237file in the instance base directory, as a JSON object of key / value
238pairs (if this file does not exist, it will be created with
239the predefined set). Each key in the object contains the text to be found (e.g.,
240the :-) for a smiling face), and its associated value, the text string that
241will replace it (in this example case, the HTML entity for the Unicode codepoint
242for the smiley or the Emoji itself as text).
243.Pp
244Emoji values can also be URLs to image files; in this case, they will not be
245substituted in the post content, but added to the 'tag' array as an ActivityPub
246standard 'Emoji' object (it's recommendable that the Emoji key be enclosed in
247colons for maximum compatilibity with other ActivityPub implementations, like
248e.g. :happydoggo:). These images can be served from an external source or from the
249.Pa static
250directory of the instance admin.
251.Pp
252If you want to disable any Emoji substitution, change the file to contain
253just an empty JSON object ({}).
254.Ss SPAM Mitigation
255There have been some SPAM attacks on the Fediverse and, as too many
256instances and server implementations out there still allow automatic
257account creation, it will only get worse.
258.Nm
259includes some (not very strong) tools for trying to survive the SPAM
260flood that will eventually happen.
261.Pp
262The
263.Ic min_account_age
264field in the main configuration file allows setting a minimum age (in
265seconds) to consider too recently created accounts suspicious of being
266a potential source of SPAM. This is a naïve assumption, because spammers
267can create accounts, let them dormant for a while and then start to use
268them. Also, some ActivityPub implementations don't even bother to return
269a creation date for their accounts, so this is not very useful.
270.Pp
271From version 2.50, post content can be filtered out by regular expressions.
272These weapons of mass destruction can be written into the
273.Ic filter_reject.txt
274file in the server base directory, one per line; if this file exists,
275all posts' content will be matched (after being stripped of HTML tags)
276against these regexes, one by one, and any match will make the post to
277be rejected. If you don't know about regular expressions, don't use this
278option (or learn about them in some tutorial, there are gazillions of
279them out there), as you and your users may start missing posts. Also,
280given that every regular expression implementation supports a different
281set of features, consider reading the documentation about the one
282implemented in your system.
236.Ss ActivityPub Support 283.Ss ActivityPub Support
237These are the following activities and objects that 284These are the following activities and objects that
238.Nm 285.Nm
@@ -421,7 +468,7 @@ This is an example of a similar configuration for the Apache2 web server:
421ProxyPreserveHost On 468ProxyPreserveHost On
422 469
423# Main web access point 470# Main web access point
424<Location /social> 471<Location /fedi>
425 ProxyPass http://127.0.0.1:8001/social 472 ProxyPass http://127.0.0.1:8001/social
426</Location> 473</Location>
427 474
@@ -481,7 +528,7 @@ an example:
481# other server configuration 528# other server configuration
482[...] 529[...]
483 530
484location "/fedi*" { 531location "/fedi/*" {
485 fastcgi socket tcp "127.0.0.1" 8001 532 fastcgi socket tcp "127.0.0.1" 8001
486} 533}
487 534
diff --git a/format.c b/format.c
index 9944822..92901bb 100644
--- a/format.c
+++ b/format.c
@@ -5,6 +5,8 @@
5#include "xs_regex.h" 5#include "xs_regex.h"
6#include "xs_mime.h" 6#include "xs_mime.h"
7#include "xs_html.h" 7#include "xs_html.h"
8#include "xs_json.h"
9#include "xs_time.h"
8 10
9#include "snac.h" 11#include "snac.h"
10 12
@@ -36,6 +38,46 @@ const char *smileys[] = {
36}; 38};
37 39
38 40
41xs_dict *emojis(void)
42/* returns a dict with the emojis */
43{
44 xs *fn = xs_fmt("%s/emojis.json", srv_basedir);
45 FILE *f;
46
47 if (mtime(fn) == 0) {
48 /* file does not exist; create it with the defaults */
49 xs *d = xs_dict_new();
50 const char **emo = smileys;
51
52 while (*emo) {
53 d = xs_dict_append(d, emo[0], emo[1]);
54 emo += 2;
55 }
56
57 if ((f = fopen(fn, "w")) != NULL) {
58 xs_json_dump(d, 4, f);
59 fclose(f);
60 }
61 else
62 srv_log(xs_fmt("Error creating '%s'", fn));
63 }
64
65 xs_dict *d = NULL;
66
67 if ((f = fopen(fn, "r")) != NULL) {
68 d = xs_json_load(f);
69 fclose(f);
70
71 if (d == NULL)
72 srv_log(xs_fmt("JSON parse error in '%s'", fn));
73 }
74 else
75 srv_log(xs_fmt("Error opening '%s'", fn));
76
77 return d;
78}
79
80
39static xs_str *format_line(const char *line, xs_list **attach) 81static xs_str *format_line(const char *line, xs_list **attach)
40/* formats a line */ 82/* formats a line */
41{ 83{
@@ -106,7 +148,7 @@ static xs_str *format_line(const char *line, xs_list **attach)
106} 148}
107 149
108 150
109xs_str *not_really_markdown(const char *content, xs_list **attach) 151xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag)
110/* formats a content using some Markdown rules */ 152/* formats a content using some Markdown rules */
111{ 153{
112 xs_str *s = xs_str_new(NULL); 154 xs_str *s = xs_str_new(NULL);
@@ -190,11 +232,36 @@ xs_str *not_really_markdown(const char *content, xs_list **attach)
190 232
191 { 233 {
192 /* traditional emoticons */ 234 /* traditional emoticons */
193 const char **emo = smileys; 235 xs *d = emojis();
194 236 int c = 0;
195 while (*emo) { 237 char *k, *v;
196 s = xs_replace_i(s, emo[0], emo[1]); 238
197 emo += 2; 239 while (xs_dict_next(d, &k, &v, &c)) {
240 const char *t = NULL;
241
242 /* is it an URL to an image? */
243 if (xs_startswith(v, "https:/" "/") && xs_startswith((t = xs_mime_by_ext(v)), "image/")) {
244 if (tag) {
245 /* add the emoji to the tag list */
246 xs *e = xs_dict_new();
247 xs *i = xs_dict_new();
248 xs *u = xs_str_utctime(0, ISO_DATE_SPEC);
249
250 e = xs_dict_append(e, "id", v);
251 e = xs_dict_append(e, "type", "Emoji");
252 e = xs_dict_append(e, "name", k);
253 e = xs_dict_append(e, "updated", u);
254
255 i = xs_dict_append(i, "type", "Image");
256 i = xs_dict_append(i, "mediaType", t);
257 i = xs_dict_append(i, "url", v);
258 e = xs_dict_append(e, "icon", i);
259
260 *tag = xs_list_append(*tag, e);
261 }
262 }
263 else
264 s = xs_replace_i(s, k, v);
198 } 265 }
199 } 266 }
200 267
diff --git a/html.c b/html.c
index 46ef735..a251e21 100644
--- a/html.c
+++ b/html.c
@@ -113,9 +113,13 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
113 xs *name = actor_name(actor); 113 xs *name = actor_name(actor);
114 114
115 /* get the avatar */ 115 /* get the avatar */
116 if ((v = xs_dict_get(actor, "icon")) != NULL && 116 if ((v = xs_dict_get(actor, "icon")) != NULL) {
117 (v = xs_dict_get(v, "url")) != NULL) { 117 /* if it's a list (Peertube), get the first one */
118 avatar = xs_dup(v); 118 if (xs_type(v) == XSTYPE_LIST)
119 v = xs_list_get(v, 0);
120
121 if ((v = xs_dict_get(v, "url")) != NULL)
122 avatar = xs_dup(v);
119 } 123 }
120 124
121 if (avatar == NULL) 125 if (avatar == NULL)
@@ -245,7 +249,7 @@ xs_html *html_msg_icon(snac *user, char *actor_id, const xs_dict *msg)
245 xs *actor = NULL; 249 xs *actor = NULL;
246 xs_html *actor_icon = NULL; 250 xs_html *actor_icon = NULL;
247 251
248 if (actor_id && valid_status(actor_get(actor_id, &actor))) { 252 if (actor_id && valid_status(actor_get_refresh(user, actor_id, &actor))) {
249 char *date = NULL; 253 char *date = NULL;
250 char *udate = NULL; 254 char *udate = NULL;
251 char *url = NULL; 255 char *url = NULL;
@@ -273,7 +277,8 @@ xs_html *html_note(snac *user, char *summary,
273 char *edit_id, char *actor_id, 277 char *edit_id, char *actor_id,
274 xs_val *cw_yn, char *cw_text, 278 xs_val *cw_yn, char *cw_text,
275 xs_val *mnt_only, char *redir, 279 xs_val *mnt_only, char *redir,
276 char *in_reply_to, int poll) 280 char *in_reply_to, int poll,
281 char *att_file, char *att_alt_text)
277{ 282{
278 xs *action = xs_fmt("%s/admin/note", user->actor); 283 xs *action = xs_fmt("%s/admin/note", user->actor);
279 284
@@ -359,19 +364,37 @@ xs_html *html_note(snac *user, char *summary,
359 xs_html_attr("name", "edit_id"), 364 xs_html_attr("name", "edit_id"),
360 xs_html_attr("value", edit_id))); 365 xs_html_attr("value", edit_id)));
361 366
367 /* attachment controls */
368 xs_html *att;
369
362 xs_html_add(form, 370 xs_html_add(form,
363 xs_html_tag("p", NULL), 371 xs_html_tag("p", NULL),
364 xs_html_tag("details", 372 att = xs_html_tag("details",
365 xs_html_tag("summary", 373 xs_html_tag("summary",
366 xs_html_text(L("Attachment..."))), 374 xs_html_text(L("Attachment..."))),
367 xs_html_tag("p", NULL), 375 xs_html_tag("p", NULL)));
368 xs_html_sctag("input", 376
369 xs_html_attr("type", "file"), 377 if (att_file && *att_file)
370 xs_html_attr("name", "attach")), 378 xs_html_add(att,
371 xs_html_sctag("input", 379 xs_html_text(L("File:")),
372 xs_html_attr("type", "text"), 380 xs_html_sctag("input",
373 xs_html_attr("name", "alt_text"), 381 xs_html_attr("type", "text"),
374 xs_html_attr("placeholder", L("Attachment description"))))); 382 xs_html_attr("name", "attach_url"),
383 xs_html_attr("title", L("Clear this field to delete the attachment")),
384 xs_html_attr("value", att_file)));
385 else
386 xs_html_add(att,
387 xs_html_sctag("input",
388 xs_html_attr("type", "file"),
389 xs_html_attr("name", "attach")));
390
391 xs_html_add(att,
392 xs_html_text(" "),
393 xs_html_sctag("input",
394 xs_html_attr("type", "text"),
395 xs_html_attr("name", "alt_text"),
396 xs_html_attr("value", att_alt_text),
397 xs_html_attr("placeholder", L("Attachment description"))));
375 398
376 /* add poll controls */ 399 /* add poll controls */
377 if (poll) { 400 if (poll) {
@@ -551,7 +574,7 @@ static xs_html *html_instance_body(char *tag)
551} 574}
552 575
553 576
554xs_html *html_user_head(snac *user, char *desc) 577xs_html *html_user_head(snac *user, char *desc, char *url)
555{ 578{
556 xs_html *head = html_base_head(); 579 xs_html *head = html_base_head();
557 580
@@ -641,6 +664,13 @@ xs_html *html_user_head(snac *user, char *desc)
641 xs_html_attr("title", "RSS"), 664 xs_html_attr("title", "RSS"),
642 xs_html_attr("href", rss_url))); 665 xs_html_attr("href", rss_url)));
643 666
667 /* ActivityPub alternate link (actor id) */
668 xs_html_add(head,
669 xs_html_sctag("link",
670 xs_html_attr("rel", "alternate"),
671 xs_html_attr("type", "application/activity+json"),
672 xs_html_attr("href", url ? url : user->actor)));
673
644 return head; 674 return head;
645} 675}
646 676
@@ -756,7 +786,7 @@ static xs_html *html_user_body(snac *user, int read_only)
756 786
757 if (read_only) { 787 if (read_only) {
758 xs *es1 = encode_html(xs_dict_get(user->config, "bio")); 788 xs *es1 = encode_html(xs_dict_get(user->config, "bio"));
759 xs *bio1 = not_really_markdown(es1, NULL); 789 xs *bio1 = not_really_markdown(es1, NULL, NULL);
760 xs *tags = xs_list_new(); 790 xs *tags = xs_list_new();
761 xs *bio2 = process_tags(user, bio1, &tags); 791 xs *bio2 = process_tags(user, bio1, &tags);
762 792
@@ -774,7 +804,7 @@ static xs_html *html_user_body(snac *user, int read_only)
774 804
775 xs_dict *val_links = user->links; 805 xs_dict *val_links = user->links;
776 if (xs_is_null(val_links)) 806 if (xs_is_null(val_links))
777 val_links = xs_stock_dict; 807 val_links = xs_stock(XSTYPE_DICT);
778 808
779 xs_html *snac_metadata = xs_html_tag("div", 809 xs_html *snac_metadata = xs_html_tag("div",
780 xs_html_attr("class", "snac-metadata")); 810 xs_html_attr("class", "snac-metadata"));
@@ -852,9 +882,9 @@ xs_html *html_top_controls(snac *snac)
852 "new_post_div", "new_post_form", 882 "new_post_div", "new_post_form",
853 L("What's on your mind?"), "", 883 L("What's on your mind?"), "",
854 NULL, NULL, 884 NULL, NULL,
855 xs_stock_false, "", 885 xs_stock(XSTYPE_FALSE), "",
856 xs_stock_false, NULL, 886 xs_stock(XSTYPE_FALSE), NULL,
857 NULL, 1), 887 NULL, 1, "", ""),
858 888
859 /** operations **/ 889 /** operations **/
860 xs_html_tag("details", 890 xs_html_tag("details",
@@ -1219,6 +1249,11 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
1219 xs_html_add(form, 1249 xs_html_add(form,
1220 html_button("like", L("Like"), L("Say you like this post"))); 1250 html_button("like", L("Like"), L("Say you like this post")));
1221 } 1251 }
1252 else {
1253 /* not like it anymore */
1254 xs_html_add(form,
1255 html_button("unlike", L("Unlike"), L("Nah don't like it that much")));
1256 }
1222 } 1257 }
1223 else { 1258 else {
1224 if (is_pinned(snac, id)) 1259 if (is_pinned(snac, id))
@@ -1235,6 +1270,11 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
1235 xs_html_add(form, 1270 xs_html_add(form,
1236 html_button("boost", L("Boost"), L("Announce this post to your followers"))); 1271 html_button("boost", L("Boost"), L("Announce this post to your followers")));
1237 } 1272 }
1273 else {
1274 /* already boosted; add button to regret */
1275 xs_html_add(form,
1276 html_button("unboost", L("Unboost"), L("I regret I boosted this")));
1277 }
1238 } 1278 }
1239 1279
1240 if (strcmp(actor, snac->actor) != 0) { 1280 if (strcmp(actor, snac->actor) != 0) {
@@ -1278,6 +1318,20 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
1278 xs *form_id = xs_fmt("%s_edit_form", md5); 1318 xs *form_id = xs_fmt("%s_edit_form", md5);
1279 xs *redir = xs_fmt("%s_entry", md5); 1319 xs *redir = xs_fmt("%s_entry", md5);
1280 1320
1321 char *att_file = "";
1322 char *att_alt_text = "";
1323 xs_list *att_list = xs_dict_get(msg, "attachment");
1324
1325 /* does it have an attachment? */
1326 if (xs_type(att_list) == XSTYPE_LIST && xs_list_len(att_list)) {
1327 xs_dict *d = xs_list_get(att_list, 0);
1328
1329 if (xs_type(d) == XSTYPE_DICT) {
1330 att_file = xs_dict_get_def(d, "url", "");
1331 att_alt_text = xs_dict_get_def(d, "name", "");
1332 }
1333 }
1334
1281 xs_html_add(controls, xs_html_tag("div", 1335 xs_html_add(controls, xs_html_tag("div",
1282 xs_html_tag("p", NULL), 1336 xs_html_tag("p", NULL),
1283 html_note(snac, L("Edit..."), 1337 html_note(snac, L("Edit..."),
@@ -1285,8 +1339,8 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
1285 "", prev_src, 1339 "", prev_src,
1286 id, NULL, 1340 id, NULL,
1287 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), 1341 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1288 xs_stock_false, redir, 1342 xs_stock(XSTYPE_FALSE), redir,
1289 NULL, 0)), 1343 NULL, 0, att_file, att_alt_text)),
1290 xs_html_tag("p", NULL)); 1344 xs_html_tag("p", NULL));
1291 } 1345 }
1292 1346
@@ -1304,8 +1358,8 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
1304 "", ct, 1358 "", ct,
1305 NULL, NULL, 1359 NULL, NULL,
1306 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), 1360 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1307 xs_stock_false, redir, 1361 xs_stock(XSTYPE_FALSE), redir,
1308 id, 0)), 1362 id, 0, "", "")),
1309 xs_html_tag("p", NULL)); 1363 xs_html_tag("p", NULL));
1310 } 1364 }
1311 1365
@@ -1858,6 +1912,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1858 1912
1859 xs_list *p = children; 1913 xs_list *p = children;
1860 char *cmd5; 1914 char *cmd5;
1915 int cnt = 0;
1916 int o_cnt = 0;
1917
1861 while (xs_list_iter(&p, &cmd5)) { 1918 while (xs_list_iter(&p, &cmd5)) {
1862 xs *chd = NULL; 1919 xs *chd = NULL;
1863 1920
@@ -1866,23 +1923,40 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1866 else 1923 else
1867 object_get_by_md5(cmd5, &chd); 1924 object_get_by_md5(cmd5, &chd);
1868 1925
1869 if (chd != NULL && xs_is_null(xs_dict_get(chd, "name"))) { 1926 if (chd != NULL) {
1870 xs_html *che = html_entry(user, chd, read_only, level + 1, cmd5, hide_children); 1927 if (xs_is_null(xs_dict_get(chd, "name"))) {
1928 xs_html *che = html_entry(user, chd, read_only,
1929 level + 1, cmd5, hide_children);
1930
1931 if (che != NULL) {
1932 if (left > 3) {
1933 xs_html_add(ch_older,
1934 che);
1935
1936 o_cnt++;
1937 }
1938 else
1939 xs_html_add(ch_container,
1940 che);
1871 1941
1872 if (che != NULL) { 1942 cnt++;
1873 if (left > 3) 1943 }
1874 xs_html_add(ch_older,
1875 che);
1876 else
1877 xs_html_add(ch_container,
1878 che);
1879 } 1944 }
1945
1946 left--;
1880 } 1947 }
1881 else 1948 else
1882 srv_debug(2, xs_fmt("cannot read child %s", cmd5)); 1949 srv_debug(2, xs_fmt("cannot read child %s", cmd5));
1883
1884 left--;
1885 } 1950 }
1951
1952 /* if no children were finally added, hide the details */
1953 if (cnt == 0)
1954 xs_html_add(ch_details,
1955 xs_html_attr("style", "display: none"));
1956
1957 if (o_cnt == 0 && ch_older)
1958 xs_html_add(ch_older,
1959 xs_html_attr("style", "display: none"));
1886 } 1960 }
1887 } 1961 }
1888 1962
@@ -1917,6 +1991,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
1917 double t = ftime(); 1991 double t = ftime();
1918 1992
1919 xs *desc = NULL; 1993 xs *desc = NULL;
1994 xs *alternate = NULL;
1920 1995
1921 if (xs_list_len(list) == 1) { 1996 if (xs_list_len(list) == 1) {
1922 /* only one element? pick the description from the source */ 1997 /* only one element? pick the description from the source */
@@ -1925,13 +2000,15 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
1925 object_get_by_md5(id, &d); 2000 object_get_by_md5(id, &d);
1926 if (d && (v = xs_dict_get(d, "sourceContent")) != NULL) 2001 if (d && (v = xs_dict_get(d, "sourceContent")) != NULL)
1927 desc = xs_dup(v); 2002 desc = xs_dup(v);
2003
2004 alternate = xs_dup(xs_dict_get(d, "id"));
1928 } 2005 }
1929 2006
1930 xs_html *head; 2007 xs_html *head;
1931 xs_html *body; 2008 xs_html *body;
1932 2009
1933 if (user) { 2010 if (user) {
1934 head = html_user_head(user, desc); 2011 head = html_user_head(user, desc, alternate);
1935 body = html_user_body(user, read_only); 2012 body = html_user_body(user, read_only);
1936 } 2013 }
1937 else { 2014 else {
@@ -1977,9 +2054,16 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
1977 if (user != NULL && !is_msg_public(msg)) { 2054 if (user != NULL && !is_msg_public(msg)) {
1978 char *irt = xs_dict_get(msg, "inReplyTo"); 2055 char *irt = xs_dict_get(msg, "inReplyTo");
1979 2056
2057 /* is it a reply to something not in the storage? */
1980 if (!xs_is_null(irt) && !object_here(irt)) { 2058 if (!xs_is_null(irt) && !object_here(irt)) {
1981 snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v)); 2059 /* is it for me? */
1982 continue; 2060 xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
2061 xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
2062
2063 if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
2064 snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
2065 continue;
2066 }
1983 } 2067 }
1984 } 2068 }
1985 2069
@@ -2077,9 +2161,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t)
2077 snac_posts = xs_html_tag("details", 2161 snac_posts = xs_html_tag("details",
2078 xs_html_attr("open", NULL), 2162 xs_html_attr("open", NULL),
2079 xs_html_tag("summary", 2163 xs_html_tag("summary",
2080 xs_html_text("...")), 2164 xs_html_text("..."))));
2081 xs_html_tag("div",
2082 xs_html_attr("class", "snac-posts"))));
2083 2165
2084 xs_list *p = list; 2166 xs_list *p = list;
2085 char *actor_id; 2167 char *actor_id;
@@ -2181,9 +2263,9 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t)
2181 dm_div_id, dm_form_id, 2263 dm_div_id, dm_form_id,
2182 "", "", 2264 "", "",
2183 NULL, actor_id, 2265 NULL, actor_id,
2184 xs_stock_false, "", 2266 xs_stock(XSTYPE_FALSE), "",
2185 xs_stock_false, NULL, 2267 xs_stock(XSTYPE_FALSE), NULL,
2186 NULL, 0), 2268 NULL, 0, "", ""),
2187 xs_html_tag("p", NULL)); 2269 xs_html_tag("p", NULL));
2188 2270
2189 xs_html_add(snac_post, snac_controls); 2271 xs_html_add(snac_post, snac_controls);
@@ -2202,10 +2284,12 @@ xs_str *html_people(snac *user)
2202 xs *wers = follower_list(user); 2284 xs *wers = follower_list(user);
2203 2285
2204 xs_html *html = xs_html_tag("html", 2286 xs_html *html = xs_html_tag("html",
2205 html_user_head(user, NULL), 2287 html_user_head(user, NULL, NULL),
2206 xs_html_add(html_user_body(user, 0), 2288 xs_html_add(html_user_body(user, 0),
2207 html_people_list(user, wing, L("People you follow"), "i"), 2289 xs_html_tag("div",
2208 html_people_list(user, wers, L("People that follow you"), "e"), 2290 xs_html_attr("class", "snac-posts"),
2291 html_people_list(user, wing, L("People you follow"), "i"),
2292 html_people_list(user, wers, L("People that follow you"), "e")),
2209 html_footer())); 2293 html_footer()));
2210 2294
2211 return xs_html_render_s(html, "<!DOCTYPE html>\n"); 2295 return xs_html_render_s(html, "<!DOCTYPE html>\n");
@@ -2220,7 +2304,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
2220 xs_html *body = html_user_body(user, 0); 2304 xs_html *body = html_user_body(user, 0);
2221 2305
2222 xs_html *html = xs_html_tag("html", 2306 xs_html *html = xs_html_tag("html",
2223 html_user_head(user, NULL), 2307 html_user_head(user, NULL, NULL),
2224 body); 2308 body);
2225 2309
2226 xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor); 2310 xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
@@ -2604,7 +2688,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
2604 return 403; 2688 return 403;
2605 2689
2606 xs *elems = timeline_simple_list(&snac, "public", 0, 20); 2690 xs *elems = timeline_simple_list(&snac, "public", 0, 20);
2607 xs *bio = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL); 2691 xs *bio = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL, NULL);
2608 2692
2609 xs *rss_title = xs_fmt("%s (@%s@%s)", 2693 xs *rss_title = xs_fmt("%s (@%s@%s)",
2610 xs_dict_get(snac.config, "name"), 2694 xs_dict_get(snac.config, "name"),
@@ -2802,7 +2886,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
2802 msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv); 2886 msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv);
2803 2887
2804 if (sensitive != NULL) { 2888 if (sensitive != NULL) {
2805 msg = xs_dict_set(msg, "sensitive", xs_stock_true); 2889 msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
2806 msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary); 2890 msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary);
2807 } 2891 }
2808 2892
@@ -2881,6 +2965,22 @@ int html_post_handler(const xs_dict *req, const char *q_path,
2881 } 2965 }
2882 } 2966 }
2883 else 2967 else
2968 if (strcmp(action, L("Unlike")) == 0) { /** **/
2969 xs *msg = msg_repulsion(&snac, id, "Like");
2970
2971 if (msg != NULL) {
2972 enqueue_message(&snac, msg);
2973 }
2974 }
2975 else
2976 if (strcmp(action, L("Unboost")) == 0) { /** **/
2977 xs *msg = msg_repulsion(&snac, id, "Announce");
2978
2979 if (msg != NULL) {
2980 enqueue_message(&snac, msg);
2981 }
2982 }
2983 else
2884 if (strcmp(action, L("MUTE")) == 0) { /** **/ 2984 if (strcmp(action, L("MUTE")) == 0) { /** **/
2885 mute(&snac, actor); 2985 mute(&snac, actor);
2886 } 2986 }
@@ -3036,17 +3136,17 @@ int html_post_handler(const xs_dict *req, const char *q_path,
3036 snac.config = xs_dict_set(snac.config, "purge_days", days); 3136 snac.config = xs_dict_set(snac.config, "purge_days", days);
3037 } 3137 }
3038 if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0) 3138 if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
3039 snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock_true); 3139 snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE));
3040 else 3140 else
3041 snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock_false); 3141 snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE));
3042 if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0) 3142 if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
3043 snac.config = xs_dict_set(snac.config, "bot", xs_stock_true); 3143 snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE));
3044 else 3144 else
3045 snac.config = xs_dict_set(snac.config, "bot", xs_stock_false); 3145 snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE));
3046 if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0) 3146 if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
3047 snac.config = xs_dict_set(snac.config, "private", xs_stock_true); 3147 snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE));
3048 else 3148 else
3049 snac.config = xs_dict_set(snac.config, "private", xs_stock_false); 3149 snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE));
3050 if ((v = xs_dict_get(p_vars, "metadata")) != NULL) { 3150 if ((v = xs_dict_get(p_vars, "metadata")) != NULL) {
3051 /* split the metadata and store it as a dict */ 3151 /* split the metadata and store it as a dict */
3052 xs_dict *md = xs_dict_new(); 3152 xs_dict *md = xs_dict_new();
diff --git a/httpd.c b/httpd.c
index d74642f..e402e61 100644
--- a/httpd.c
+++ b/httpd.c
@@ -388,6 +388,7 @@ void httpd_connection(FILE *f)
388 body, xs_dict_get(srv_config, "host")); 388 body, xs_dict_get(srv_config, "host"));
389 389
390 headers = xs_dict_append(headers, "WWW-Authenticate", www_auth); 390 headers = xs_dict_append(headers, "WWW-Authenticate", www_auth);
391 headers = xs_dict_append(headers, "Cache-Control", "no-cache, must-revalidate, max-age=0");
391 } 392 }
392 393
393 if (ctype == NULL) 394 if (ctype == NULL)
@@ -814,7 +815,7 @@ void httpd(void)
814 815
815 /* send as many exit jobs as working threads */ 816 /* send as many exit jobs as working threads */
816 for (n = 1; n < p_state->n_threads; n++) 817 for (n = 1; n < p_state->n_threads; n++)
817 job_post(xs_stock_false, 0); 818 job_post(xs_stock(XSTYPE_FALSE), 0);
818 819
819 /* wait for all the threads to exit */ 820 /* wait for all the threads to exit */
820 for (n = 0; n < p_state->n_threads; n++) 821 for (n = 0; n < p_state->n_threads; n++)
diff --git a/main.c b/main.c
index cbd9921..06cae78 100644
--- a/main.c
+++ b/main.c
@@ -5,6 +5,7 @@
5#include "xs_io.h" 5#include "xs_io.h"
6#include "xs_json.h" 6#include "xs_json.h"
7#include "xs_time.h" 7#include "xs_time.h"
8#include "xs_openssl.h"
8 9
9#include "snac.h" 10#include "snac.h"
10 11
@@ -17,30 +18,32 @@ int usage(void)
17 printf("\n"); 18 printf("\n");
18 printf("Commands:\n"); 19 printf("Commands:\n");
19 printf("\n"); 20 printf("\n");
20 printf("init [{basedir}] Initializes the data storage\n"); 21 printf("init [{basedir}] Initializes the data storage\n");
21 printf("upgrade {basedir} Upgrade to a new version\n"); 22 printf("upgrade {basedir} Upgrade to a new version\n");
22 printf("adduser {basedir} [{uid}] Adds a new user\n"); 23 printf("adduser {basedir} [{uid}] Adds a new user\n");
23 printf("deluser {basedir} {uid} Deletes a user\n"); 24 printf("deluser {basedir} {uid} Deletes a user\n");
24 printf("httpd {basedir} Starts the HTTPD daemon\n"); 25 printf("httpd {basedir} Starts the HTTPD daemon\n");
25 printf("purge {basedir} Purges old data\n"); 26 printf("purge {basedir} Purges old data\n");
26 printf("state {basedir} Prints server state\n"); 27 printf("state {basedir} Prints server state\n");
27 printf("webfinger {basedir} {actor} Queries about an actor (@user@host or actor url)\n"); 28 printf("webfinger {basedir} {actor} Queries about an actor (@user@host or actor url)\n");
28 printf("queue {basedir} {uid} Processes a user queue\n"); 29 printf("queue {basedir} {uid} Processes a user queue\n");
29 printf("follow {basedir} {uid} {actor} Follows an actor\n"); 30 printf("follow {basedir} {uid} {actor} Follows an actor\n");
30 printf("unfollow {basedir} {uid} {actor} Unfollows an actor\n"); 31 printf("unfollow {basedir} {uid} {actor} Unfollows an actor\n");
31 printf("request {basedir} {uid} {url} Requests an object\n"); 32 printf("request {basedir} {uid} {url} Requests an object\n");
32 printf("actor {basedir} [{uid}] {url} Requests an actor\n"); 33 printf("actor {basedir} [{uid}] {url} Requests an actor\n");
33 printf("note {basedir} {uid} {'text'} Sends a note to followers\n"); 34 printf("note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n");
34 printf("resetpwd {basedir} {uid} Resets the password of a user\n"); 35 printf("boost|announce {basedir} {uid} {url} Boosts (announces) a post\n");
35 printf("ping {basedir} {uid} {actor} Pings an actor\n"); 36 printf("unboost {basedir} {uid} {url} Unboosts a post\n");
36 printf("webfinger_s {basedir} {uid} {actor} Queries about an actor (@user@host or actor url)\n"); 37 printf("resetpwd {basedir} {uid} Resets the password of a user\n");
37 printf("pin {basedir} {uid} {msg_url} Pins a message\n"); 38 printf("ping {basedir} {uid} {actor} Pings an actor\n");
38 printf("unpin {basedir} {uid} {msg_url} Unpins a message\n"); 39 printf("webfinger_s {basedir} {uid} {actor} Queries about an actor (@user@host or actor url)\n");
39 printf("block {basedir} {instance_url} Blocks a full instance\n"); 40 printf("pin {basedir} {uid} {msg_url} Pins a message\n");
40 printf("unblock {basedir} {instance_url} Unblocks a full instance\n"); 41 printf("unpin {basedir} {uid} {msg_url} Unpins a message\n");
41 printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n"); 42 printf("block {basedir} {instance_url} Blocks a full instance\n");
42 printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n"); 43 printf("unblock {basedir} {instance_url} Unblocks a full instance\n");
43 printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n"); 44 printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n");
45 printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n");
46 printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n");
44 47
45 return 1; 48 return 1;
46} 49}
@@ -96,7 +99,7 @@ int main(int argc, char *argv[])
96 if (strcmp(cmd, "markdown") == 0) { /** **/ 99 if (strcmp(cmd, "markdown") == 0) { /** **/
97 /* undocumented, for testing only */ 100 /* undocumented, for testing only */
98 xs *c = xs_readall(stdin); 101 xs *c = xs_readall(stdin);
99 xs *fc = not_really_markdown(c, NULL); 102 xs *fc = not_really_markdown(c, NULL, NULL);
100 103
101 printf("<html>\n%s\n</html>\n", fc); 104 printf("<html>\n%s\n</html>\n", fc);
102 return 0; 105 return 0;
@@ -279,7 +282,7 @@ int main(int argc, char *argv[])
279 return 0; 282 return 0;
280 } 283 }
281 284
282 if (strcmp(cmd, "announce") == 0) { /** **/ 285 if (strcmp(cmd, "boost") == 0 || strcmp(cmd, "announce") == 0) { /** **/
283 xs *msg = msg_admiration(&snac, url, "Announce"); 286 xs *msg = msg_admiration(&snac, url, "Announce");
284 287
285 if (msg != NULL) { 288 if (msg != NULL) {
@@ -293,6 +296,20 @@ int main(int argc, char *argv[])
293 return 0; 296 return 0;
294 } 297 }
295 298
299 if (strcmp(cmd, "unboost") == 0) { /** **/
300 xs *msg = msg_repulsion(&snac, url, "Announce");
301
302 if (msg != NULL) {
303 enqueue_message(&snac, msg);
304
305 if (dbglevel) {
306 xs_json_dump(msg, 4, stdout);
307 }
308 }
309
310 return 0;
311 }
312
296 if (strcmp(cmd, "follow") == 0) { /** **/ 313 if (strcmp(cmd, "follow") == 0) { /** **/
297 xs *msg = msg_follow(&snac, url); 314 xs *msg = msg_follow(&snac, url);
298 315
@@ -360,6 +377,14 @@ int main(int argc, char *argv[])
360 if (strcmp(cmd, "ping") == 0) { /** **/ 377 if (strcmp(cmd, "ping") == 0) { /** **/
361 xs *actor_o = NULL; 378 xs *actor_o = NULL;
362 379
380 if (!xs_startswith(url, "https:/")) {
381 /* try to resolve via webfinger */
382 if (!valid_status(webfinger_request(url, &url, NULL))) {
383 srv_log(xs_fmt("cannot resolve %s via webfinger", url));
384 return 1;
385 }
386 }
387
363 if (valid_status(actor_request(&snac, url, &actor_o))) { 388 if (valid_status(actor_request(&snac, url, &actor_o))) {
364 xs *msg = msg_ping(&snac, url); 389 xs *msg = msg_ping(&snac, url);
365 390
@@ -368,6 +393,8 @@ int main(int argc, char *argv[])
368 if (dbglevel) { 393 if (dbglevel) {
369 xs_json_dump(msg, 4, stdout); 394 xs_json_dump(msg, 4, stdout);
370 } 395 }
396
397 srv_log(xs_fmt("Ping sent to %s -- see log for Pong reply", url));
371 } 398 }
372 else { 399 else {
373 srv_log(xs_fmt("Error getting actor %s", url)); 400 srv_log(xs_fmt("Error getting actor %s", url));
@@ -450,7 +477,39 @@ int main(int argc, char *argv[])
450 xs *content = NULL; 477 xs *content = NULL;
451 xs *msg = NULL; 478 xs *msg = NULL;
452 xs *c_msg = NULL; 479 xs *c_msg = NULL;
453 char *in_reply_to = GET_ARGV(); 480 xs *attl = xs_list_new();
481 char *fn = NULL;
482
483 /* iterate possible attachments */
484 while ((fn = GET_ARGV())) {
485 FILE *f;
486
487 if ((f = fopen(fn, "rb")) != NULL) {
488 /* get the file size and content */
489 fseek(f, 0, SEEK_END);
490 int sz = ftell(f);
491 fseek(f, 0, SEEK_SET);
492 xs *atc = xs_readall(f);
493 fclose(f);
494
495 char *ext = strrchr(fn, '.');
496 xs *hash = xs_md5_hex(fn, strlen(fn));
497 xs *id = xs_fmt("%s%s", hash, ext);
498 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
499
500 /* store */
501 static_put(&snac, id, atc, sz);
502
503 xs *l = xs_list_new();
504
505 l = xs_list_append(l, url);
506 l = xs_list_append(l, ""); /* alt text */
507
508 attl = xs_list_append(attl, l);
509 }
510 else
511 fprintf(stderr, "Error opening '%s' as attachment\n", fn);
512 }
454 513
455 if (strcmp(url, "-e") == 0) { 514 if (strcmp(url, "-e") == 0) {
456 /* get the content from an editor */ 515 /* get the content from an editor */
@@ -478,7 +537,7 @@ int main(int argc, char *argv[])
478 else 537 else
479 content = xs_dup(url); 538 content = xs_dup(url);
480 539
481 msg = msg_note(&snac, content, NULL, in_reply_to, NULL, 0); 540 msg = msg_note(&snac, content, NULL, NULL, attl, 0);
482 541
483 c_msg = msg_create(&snac, msg); 542 c_msg = msg_create(&snac, msg);
484 543
diff --git a/mastoapi.c b/mastoapi.c
index d702c47..78fd802 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -289,7 +289,11 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
289 *body = xs_dup(code); 289 *body = xs_dup(code);
290 } 290 }
291 else { 291 else {
292 *body = xs_fmt("%s?code=%s", redir, code); 292 if (xs_str_in(redir, "?"))
293 *body = xs_fmt("%s&code=%s", redir, code);
294 else
295 *body = xs_fmt("%s?code=%s", redir, code);
296
293 status = 303; 297 status = 303;
294 } 298 }
295 299
@@ -335,8 +339,8 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
335 /* FIXME: this 'scope' parameter is mandatory for the official Mastodon API, 339 /* FIXME: this 'scope' parameter is mandatory for the official Mastodon API,
336 but if it's enabled, it makes it crash after some more steps, which 340 but if it's enabled, it makes it crash after some more steps, which
337 is FAR WORSE */ 341 is FAR WORSE */
338// const char *scope = xs_dict_get(args, "scope");
339 const char *scope = NULL; 342 const char *scope = NULL;
343// scope = xs_dict_get(args, "scope");
340 344
341 /* no client_secret? check if it's inside an authorization header 345 /* no client_secret? check if it's inside an authorization header
342 (AndStatus does it this way) */ 346 (AndStatus does it this way) */
@@ -359,9 +363,9 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
359 } 363 }
360 } 364 }
361 365
362 /* no code? 366 /* no code?
363 I'm not sure of the impacts of this right now, but Subway Tooter does not 367 I'm not sure of the impacts of this right now, but Subway Tooter does not
364 provide a code so one must be generated */ 368 provide a code so one must be generated */
365 if (xs_is_null(code)){ 369 if (xs_is_null(code)){
366 code = random_str(); 370 code = random_str();
367 } 371 }
@@ -522,6 +526,12 @@ xs_dict *mastoapi_account(const xs_dict *actor)
522 acct = xs_dict_append(acct, "id", acct_md5); 526 acct = xs_dict_append(acct, "id", acct_md5);
523 acct = xs_dict_append(acct, "username", prefu); 527 acct = xs_dict_append(acct, "username", prefu);
524 acct = xs_dict_append(acct, "display_name", display_name); 528 acct = xs_dict_append(acct, "display_name", display_name);
529 acct = xs_dict_append(acct, "discoverable", xs_stock(XSTYPE_TRUE));
530 acct = xs_dict_append(acct, "group", xs_stock(XSTYPE_FALSE));
531 acct = xs_dict_append(acct, "hide_collections", xs_stock(XSTYPE_FALSE));
532 acct = xs_dict_append(acct, "indexable", xs_stock(XSTYPE_TRUE));
533 acct = xs_dict_append(acct, "noindex", xs_stock(XSTYPE_FALSE));
534 acct = xs_dict_append(acct, "roles", xs_stock(XSTYPE_LIST));
525 535
526 { 536 {
527 /* create the acct field as user@host */ 537 /* create the acct field as user@host */
@@ -543,13 +553,14 @@ xs_dict *mastoapi_account(const xs_dict *actor)
543 note = ""; 553 note = "";
544 554
545 if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) 555 if (strcmp(xs_dict_get(actor, "type"), "Service") == 0)
546 acct = xs_dict_append(acct, "bot", xs_stock_true); 556 acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_TRUE));
547 else 557 else
548 acct = xs_dict_append(acct, "bot", xs_stock_false); 558 acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_FALSE));
549 559
550 acct = xs_dict_append(acct, "note", note); 560 acct = xs_dict_append(acct, "note", note);
551 561
552 acct = xs_dict_append(acct, "url", id); 562 acct = xs_dict_append(acct, "url", id);
563 acct = xs_dict_append(acct, "uri", id);
553 564
554 xs *avatar = NULL; 565 xs *avatar = NULL;
555 xs_dict *av = xs_dict_get(actor, "icon"); 566 xs_dict *av = xs_dict_get(actor, "icon");
@@ -574,7 +585,7 @@ xs_dict *mastoapi_account(const xs_dict *actor)
574 header = xs_dup(xs_dict_get(hd, "url")); 585 header = xs_dup(xs_dict_get(hd, "url"));
575 586
576 if (xs_is_null(header)) 587 if (xs_is_null(header))
577 header = xs_dup(""); 588 header = xs_fmt("%s/header.png", srv_baseurl);
578 589
579 acct = xs_dict_append(acct, "header", header); 590 acct = xs_dict_append(acct, "header", header);
580 acct = xs_dict_append(acct, "header_static", header); 591 acct = xs_dict_append(acct, "header_static", header);
@@ -602,7 +613,7 @@ xs_dict *mastoapi_account(const xs_dict *actor)
602 d1 = xs_dict_append(d1, "shortcode", nm); 613 d1 = xs_dict_append(d1, "shortcode", nm);
603 d1 = xs_dict_append(d1, "url", url); 614 d1 = xs_dict_append(d1, "url", url);
604 d1 = xs_dict_append(d1, "static_url", url); 615 d1 = xs_dict_append(d1, "static_url", url);
605 d1 = xs_dict_append(d1, "visible_in_picker", xs_stock_true); 616 d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
606 617
607 eml = xs_list_append(eml, d1); 618 eml = xs_list_append(eml, d1);
608 } 619 }
@@ -613,10 +624,10 @@ xs_dict *mastoapi_account(const xs_dict *actor)
613 acct = xs_dict_append(acct, "emojis", eml); 624 acct = xs_dict_append(acct, "emojis", eml);
614 } 625 }
615 626
616 acct = xs_dict_append(acct, "locked", xs_stock_false); 627 acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
617 acct = xs_dict_append(acct, "followers_count", xs_stock_0); 628 acct = xs_dict_append(acct, "followers_count", xs_stock(0));
618 acct = xs_dict_append(acct, "following_count", xs_stock_0); 629 acct = xs_dict_append(acct, "following_count", xs_stock(0));
619 acct = xs_dict_append(acct, "statuses_count", xs_stock_0); 630 acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
620 631
621 xs *fields = xs_list_new(); 632 xs *fields = xs_list_new();
622 p = xs_dict_get(actor, "attachment"); 633 p = xs_dict_get(actor, "attachment");
@@ -624,19 +635,19 @@ xs_dict *mastoapi_account(const xs_dict *actor)
624 635
625 /* dict of validated links */ 636 /* dict of validated links */
626 xs_dict *val_links = NULL; 637 xs_dict *val_links = NULL;
627 xs_dict *metadata = xs_stock_dict; 638 xs_dict *metadata = xs_stock(XSTYPE_DICT);
628 snac user = {0}; 639 snac user = {0};
629 640
630 if (xs_startswith(id, srv_baseurl)) { 641 if (xs_startswith(id, srv_baseurl)) {
631 /* if it's a local user, open it and pick its validated links */ 642 /* if it's a local user, open it and pick its validated links */
632 if (user_open(&user, prefu)) { 643 if (user_open(&user, prefu)) {
633 val_links = user.links; 644 val_links = user.links;
634 metadata = xs_dict_get_def(user.config, "metadata", xs_stock_dict); 645 metadata = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT));
635 } 646 }
636 } 647 }
637 648
638 if (xs_is_null(val_links)) 649 if (xs_is_null(val_links))
639 val_links = xs_stock_dict; 650 val_links = xs_stock(XSTYPE_DICT);
640 651
641 while (xs_list_iter(&p, &v)) { 652 while (xs_list_iter(&p, &v)) {
642 char *type = xs_dict_get(v, "type"); 653 char *type = xs_dict_get(v, "type");
@@ -665,7 +676,7 @@ xs_dict *mastoapi_account(const xs_dict *actor)
665 d = xs_dict_append(d, "value", value); 676 d = xs_dict_append(d, "value", value);
666 d = xs_dict_append(d, "verified_at", 677 d = xs_dict_append(d, "verified_at",
667 xs_type(val_date) == XSTYPE_STRING && *val_date ? 678 xs_type(val_date) == XSTYPE_STRING && *val_date ?
668 val_date : xs_stock_null); 679 val_date : xs_stock(XSTYPE_NULL));
669 680
670 fields = xs_list_append(fields, d); 681 fields = xs_list_append(fields, d);
671 } 682 }
@@ -703,13 +714,13 @@ xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg)
703 xs *fd = mastoapi_date(xs_dict_get(msg, "endTime")); 714 xs *fd = mastoapi_date(xs_dict_get(msg, "endTime"));
704 poll = xs_dict_append(poll, "expires_at", fd); 715 poll = xs_dict_append(poll, "expires_at", fd);
705 poll = xs_dict_append(poll, "expired", 716 poll = xs_dict_append(poll, "expired",
706 xs_dict_get(msg, "closed") != NULL ? xs_stock_true : xs_stock_false); 717 xs_dict_get(msg, "closed") != NULL ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
707 718
708 if ((opts = xs_dict_get(msg, "oneOf")) != NULL) 719 if ((opts = xs_dict_get(msg, "oneOf")) != NULL)
709 poll = xs_dict_append(poll, "multiple", xs_stock_false); 720 poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_FALSE));
710 else { 721 else {
711 opts = xs_dict_get(msg, "anyOf"); 722 opts = xs_dict_get(msg, "anyOf");
712 poll = xs_dict_append(poll, "multiple", xs_stock_true); 723 poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_TRUE));
713 } 724 }
714 725
715 while (xs_list_iter(&opts, &v)) { 726 while (xs_list_iter(&opts, &v)) {
@@ -736,7 +747,7 @@ xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg)
736 747
737 poll = xs_dict_append(poll, "voted", 748 poll = xs_dict_append(poll, "voted",
738 (snac && was_question_voted(snac, xs_dict_get(msg, "id"))) ? 749 (snac && was_question_voted(snac, xs_dict_get(msg, "id"))) ?
739 xs_stock_true : xs_stock_false); 750 xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
740 751
741 return poll; 752 return poll;
742} 753}
@@ -746,7 +757,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
746/* converts an ActivityPub note to a Mastodon status */ 757/* converts an ActivityPub note to a Mastodon status */
747{ 758{
748 xs *actor = NULL; 759 xs *actor = NULL;
749 actor_get(get_atto(msg), &actor); 760 actor_get_refresh(snac, get_atto(msg), &actor);
750 761
751 /* if the author is not here, discard */ 762 /* if the author is not here, discard */
752 if (actor == NULL) 763 if (actor == NULL)
@@ -802,7 +813,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
802 813
803 tmp = xs_dict_get(msg, "sensitive"); 814 tmp = xs_dict_get(msg, "sensitive");
804 if (xs_is_null(tmp)) 815 if (xs_is_null(tmp))
805 tmp = xs_stock_false; 816 tmp = xs_stock(XSTYPE_FALSE);
806 817
807 st = xs_dict_append(st, "sensitive", tmp); 818 st = xs_dict_append(st, "sensitive", tmp);
808 819
@@ -921,7 +932,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
921 d1 = xs_dict_append(d1, "shortcode", nm); 932 d1 = xs_dict_append(d1, "shortcode", nm);
922 d1 = xs_dict_append(d1, "url", url); 933 d1 = xs_dict_append(d1, "url", url);
923 d1 = xs_dict_append(d1, "static_url", url); 934 d1 = xs_dict_append(d1, "static_url", url);
924 d1 = xs_dict_append(d1, "visible_in_picker", xs_stock_true); 935 d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
925 d1 = xs_dict_append(d1, "category", "Emojis"); 936 d1 = xs_dict_append(d1, "category", "Emojis");
926 937
927 eml = xs_list_append(eml, d1); 938 eml = xs_list_append(eml, d1);
@@ -942,7 +953,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
942 953
943 st = xs_dict_append(st, "favourites_count", ixc); 954 st = xs_dict_append(st, "favourites_count", ixc);
944 st = xs_dict_append(st, "favourited", 955 st = xs_dict_append(st, "favourited",
945 (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock_true : xs_stock_false); 956 (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
946 957
947 xs_free(idx); 958 xs_free(idx);
948 xs_free(ixc); 959 xs_free(ixc);
@@ -951,7 +962,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
951 962
952 st = xs_dict_append(st, "reblogs_count", ixc); 963 st = xs_dict_append(st, "reblogs_count", ixc);
953 st = xs_dict_append(st, "reblogged", 964 st = xs_dict_append(st, "reblogged",
954 (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock_true : xs_stock_false); 965 (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
955 966
956 /* get the last person who boosted this */ 967 /* get the last person who boosted this */
957 xs *boosted_by_md5 = NULL; 968 xs *boosted_by_md5 = NULL;
@@ -966,8 +977,8 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
966 st = xs_dict_append(st, "replies_count", ixc); 977 st = xs_dict_append(st, "replies_count", ixc);
967 978
968 /* default in_reply_to values */ 979 /* default in_reply_to values */
969 st = xs_dict_append(st, "in_reply_to_id", xs_stock_null); 980 st = xs_dict_append(st, "in_reply_to_id", xs_stock(XSTYPE_NULL));
970 st = xs_dict_append(st, "in_reply_to_account_id", xs_stock_null); 981 st = xs_dict_append(st, "in_reply_to_account_id", xs_stock(XSTYPE_NULL));
971 982
972 tmp = xs_dict_get(msg, "inReplyTo"); 983 tmp = xs_dict_get(msg, "inReplyTo");
973 if (!xs_is_null(tmp)) { 984 if (!xs_is_null(tmp)) {
@@ -985,9 +996,9 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
985 } 996 }
986 } 997 }
987 998
988 st = xs_dict_append(st, "reblog", xs_stock_null); 999 st = xs_dict_append(st, "reblog", xs_stock(XSTYPE_NULL));
989 st = xs_dict_append(st, "card", xs_stock_null); 1000 st = xs_dict_append(st, "card", xs_stock(XSTYPE_NULL));
990 st = xs_dict_append(st, "language", xs_stock_null); 1001 st = xs_dict_append(st, "language", xs_stock(XSTYPE_NULL));
991 1002
992 tmp = xs_dict_get(msg, "sourceContent"); 1003 tmp = xs_dict_get(msg, "sourceContent");
993 if (xs_is_null(tmp)) 1004 if (xs_is_null(tmp))
@@ -998,7 +1009,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
998 tmp = xs_dict_get(msg, "updated"); 1009 tmp = xs_dict_get(msg, "updated");
999 xs *fd2 = NULL; 1010 xs *fd2 = NULL;
1000 if (xs_is_null(tmp)) 1011 if (xs_is_null(tmp))
1001 tmp = xs_stock_null; 1012 tmp = xs_stock(XSTYPE_NULL);
1002 else { 1013 else {
1003 fd2 = mastoapi_date(tmp); 1014 fd2 = mastoapi_date(tmp);
1004 tmp = fd2; 1015 tmp = fd2;
@@ -1011,12 +1022,12 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
1011 st = xs_dict_append(st, "poll", poll); 1022 st = xs_dict_append(st, "poll", poll);
1012 } 1023 }
1013 else 1024 else
1014 st = xs_dict_append(st, "poll", xs_stock_null); 1025 st = xs_dict_append(st, "poll", xs_stock(XSTYPE_NULL));
1015 1026
1016 st = xs_dict_append(st, "bookmarked", xs_stock_false); 1027 st = xs_dict_append(st, "bookmarked", xs_stock(XSTYPE_FALSE));
1017 1028
1018 st = xs_dict_append(st, "pinned", 1029 st = xs_dict_append(st, "pinned",
1019 (snac && is_pinned(snac, id)) ? xs_stock_true : xs_stock_false); 1030 (snac && is_pinned(snac, id)) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1020 1031
1021 /* is it a boost? */ 1032 /* is it a boost? */
1022 if (!xs_is_null(boosted_by_md5)) { 1033 if (!xs_is_null(boosted_by_md5)) {
@@ -1060,21 +1071,21 @@ xs_dict *mastoapi_relationship(snac *snac, const char *md5)
1060 1071
1061 rel = xs_dict_append(rel, "id", md5); 1072 rel = xs_dict_append(rel, "id", md5);
1062 rel = xs_dict_append(rel, "following", 1073 rel = xs_dict_append(rel, "following",
1063 following_check(snac, actor) ? xs_stock_true : xs_stock_false); 1074 following_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1064 1075
1065 rel = xs_dict_append(rel, "showing_reblogs", xs_stock_true); 1076 rel = xs_dict_append(rel, "showing_reblogs", xs_stock(XSTYPE_TRUE));
1066 rel = xs_dict_append(rel, "notifying", xs_stock_false); 1077 rel = xs_dict_append(rel, "notifying", xs_stock(XSTYPE_FALSE));
1067 rel = xs_dict_append(rel, "followed_by", 1078 rel = xs_dict_append(rel, "followed_by",
1068 follower_check(snac, actor) ? xs_stock_true : xs_stock_false); 1079 follower_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1069 1080
1070 rel = xs_dict_append(rel, "blocking", 1081 rel = xs_dict_append(rel, "blocking",
1071 is_muted(snac, actor) ? xs_stock_true : xs_stock_false); 1082 is_muted(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1072 1083
1073 rel = xs_dict_append(rel, "muting", xs_stock_false); 1084 rel = xs_dict_append(rel, "muting", xs_stock(XSTYPE_FALSE));
1074 rel = xs_dict_append(rel, "muting_notifications", xs_stock_false); 1085 rel = xs_dict_append(rel, "muting_notifications", xs_stock(XSTYPE_FALSE));
1075 rel = xs_dict_append(rel, "requested", xs_stock_false); 1086 rel = xs_dict_append(rel, "requested", xs_stock(XSTYPE_FALSE));
1076 rel = xs_dict_append(rel, "domain_blocking", xs_stock_false); 1087 rel = xs_dict_append(rel, "domain_blocking", xs_stock(XSTYPE_FALSE));
1077 rel = xs_dict_append(rel, "endorsed", xs_stock_false); 1088 rel = xs_dict_append(rel, "endorsed", xs_stock(XSTYPE_FALSE));
1078 rel = xs_dict_append(rel, "note", ""); 1089 rel = xs_dict_append(rel, "note", "");
1079 } 1090 }
1080 1091
@@ -1142,9 +1153,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1142 acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac1.config, "published")); 1153 acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac1.config, "published"));
1143 acct = xs_dict_append(acct, "note", xs_dict_get(snac1.config, "bio")); 1154 acct = xs_dict_append(acct, "note", xs_dict_get(snac1.config, "bio"));
1144 acct = xs_dict_append(acct, "url", snac1.actor); 1155 acct = xs_dict_append(acct, "url", snac1.actor);
1145 acct = xs_dict_append(acct, "header", ""); 1156 acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
1146 acct = xs_dict_append(acct, "header_static", "");
1147 acct = xs_dict_append(acct, "locked", xs_stock_false);
1148 acct = xs_dict_append(acct, "bot", xs_dict_get(snac1.config, "bot")); 1157 acct = xs_dict_append(acct, "bot", xs_dict_get(snac1.config, "bot"));
1149 1158
1150 xs *src = xs_json_loads("{\"privacy\":\"public\"," 1159 xs *src = xs_json_loads("{\"privacy\":\"public\","
@@ -1162,6 +1171,17 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1162 acct = xs_dict_append(acct, "avatar", avatar); 1171 acct = xs_dict_append(acct, "avatar", avatar);
1163 acct = xs_dict_append(acct, "avatar_static", avatar); 1172 acct = xs_dict_append(acct, "avatar_static", avatar);
1164 1173
1174 xs *header = NULL;
1175 char *hd = xs_dict_get(snac1.config, "header");
1176
1177 if (!xs_is_null(hd))
1178 header = xs_dup(hd);
1179 else
1180 header = xs_fmt("%s/header.png", srv_baseurl);
1181
1182 acct = xs_dict_append(acct, "header", header);
1183 acct = xs_dict_append(acct, "header_static", header);
1184
1165 xs_dict *metadata = xs_dict_get(snac1.config, "metadata"); 1185 xs_dict *metadata = xs_dict_get(snac1.config, "metadata");
1166 if (xs_type(metadata) == XSTYPE_DICT) { 1186 if (xs_type(metadata) == XSTYPE_DICT) {
1167 xs *fields = xs_list_new(); 1187 xs *fields = xs_list_new();
@@ -1170,7 +1190,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1170 1190
1171 xs_dict *val_links = snac1.links; 1191 xs_dict *val_links = snac1.links;
1172 if (xs_is_null(val_links)) 1192 if (xs_is_null(val_links))
1173 val_links = xs_stock_dict; 1193 val_links = xs_stock(XSTYPE_DICT);
1174 1194
1175 int c = 0; 1195 int c = 0;
1176 while (xs_dict_next(metadata, &k, &v, &c)) { 1196 while (xs_dict_next(metadata, &k, &v, &c)) {
@@ -1190,7 +1210,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1190 d = xs_dict_append(d, "value", v); 1210 d = xs_dict_append(d, "value", v);
1191 d = xs_dict_append(d, "verified_at", 1211 d = xs_dict_append(d, "verified_at",
1192 xs_type(val_date) == XSTYPE_STRING && *val_date ? 1212 xs_type(val_date) == XSTYPE_STRING && *val_date ?
1193 val_date : xs_stock_null); 1213 val_date : xs_stock(XSTYPE_NULL));
1194 1214
1195 fields = xs_list_append(fields, d); 1215 fields = xs_list_append(fields, d);
1196 } 1216 }
@@ -1198,9 +1218,9 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1198 acct = xs_dict_set(acct, "fields", fields); 1218 acct = xs_dict_set(acct, "fields", fields);
1199 } 1219 }
1200 1220
1201 acct = xs_dict_append(acct, "followers_count", xs_stock_0); 1221 acct = xs_dict_append(acct, "followers_count", xs_stock(0));
1202 acct = xs_dict_append(acct, "following_count", xs_stock_0); 1222 acct = xs_dict_append(acct, "following_count", xs_stock(0));
1203 acct = xs_dict_append(acct, "statuses_count", xs_stock_0); 1223 acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
1204 1224
1205 *body = xs_json_dumps(acct, 4); 1225 *body = xs_json_dumps(acct, 4);
1206 *ctype = "application/json"; 1226 *ctype = "application/json";
@@ -1716,8 +1736,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1716 else 1736 else
1717 if (strcmp(cmd, "/v2/filters") == 0) { /** **/ 1737 if (strcmp(cmd, "/v2/filters") == 0) { /** **/
1718 /* snac will never have filters 1738 /* snac will never have filters
1719 * but still, without a v2 endpoint a short delay is introduced 1739 * but still, without a v2 endpoint a short delay is introduced
1720 * in some apps */ 1740 * in some apps */
1721 *body = xs_dup("[]"); 1741 *body = xs_dup("[]");
1722 *ctype = "application/json"; 1742 *ctype = "application/json";
1723 status = 200; 1743 status = 200;
@@ -1797,7 +1817,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1797 1817
1798 ins = xs_dict_append(ins, "email", v); 1818 ins = xs_dict_append(ins, "email", v);
1799 1819
1800 ins = xs_dict_append(ins, "rules", xs_stock_list); 1820 ins = xs_dict_append(ins, "rules", xs_stock(XSTYPE_LIST));
1801 1821
1802 xs *l1 = xs_list_append(xs_list_new(), "en"); 1822 xs *l1 = xs_list_append(xs_list_new(), "en");
1803 ins = xs_dict_append(ins, "languages", l1); 1823 ins = xs_dict_append(ins, "languages", l1);
@@ -1808,14 +1828,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1808 1828
1809 ins = xs_dict_append(ins, "urls", urls); 1829 ins = xs_dict_append(ins, "urls", urls);
1810 1830
1811 xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock_0); 1831 xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock(0));
1812 d2 = xs_dict_append(d2, "status_count", xs_stock_0); 1832 d2 = xs_dict_append(d2, "status_count", xs_stock(0));
1813 d2 = xs_dict_append(d2, "domain_count", xs_stock_0); 1833 d2 = xs_dict_append(d2, "domain_count", xs_stock(0));
1814 ins = xs_dict_append(ins, "stats", d2); 1834 ins = xs_dict_append(ins, "stats", d2);
1815 1835
1816 ins = xs_dict_append(ins, "registrations", xs_stock_false); 1836 ins = xs_dict_append(ins, "registrations", xs_stock(XSTYPE_FALSE));
1817 ins = xs_dict_append(ins, "approval_required", xs_stock_false); 1837 ins = xs_dict_append(ins, "approval_required", xs_stock(XSTYPE_FALSE));
1818 ins = xs_dict_append(ins, "invites_enabled", xs_stock_false); 1838 ins = xs_dict_append(ins, "invites_enabled", xs_stock(XSTYPE_FALSE));
1819 1839
1820 xs *cfg = xs_dict_new(); 1840 xs *cfg = xs_dict_new();
1821 1841
@@ -2063,7 +2083,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
2063 d = xs_dict_append(d, "name", q); 2083 d = xs_dict_append(d, "name", q);
2064 xs *url = xs_fmt("%s?t=%s", srv_baseurl, q); 2084 xs *url = xs_fmt("%s?t=%s", srv_baseurl, q);
2065 d = xs_dict_append(d, "url", url); 2085 d = xs_dict_append(d, "url", url);
2066 d = xs_dict_append(d, "history", xs_stock_list); 2086 d = xs_dict_append(d, "history", xs_stock(XSTYPE_LIST));
2067 2087
2068 htl = xs_list_append(htl, d); 2088 htl = xs_list_append(htl, d);
2069 } 2089 }
@@ -2103,8 +2123,6 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2103 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) 2123 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
2104 return 0; 2124 return 0;
2105 2125
2106 srv_debug(1, xs_fmt("mastoapi_post_handler %s", q_path));
2107
2108 int status = 404; 2126 int status = 404;
2109 xs *args = NULL; 2127 xs *args = NULL;
2110 char *i_ctype = xs_dict_get(req, "content-type"); 2128 char *i_ctype = xs_dict_get(req, "content-type");
@@ -2115,7 +2133,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2115 } 2133 }
2116 else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded")) 2134 else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
2117 { 2135 {
2118 // Some apps send form data instead of json so we should cater for those 2136 // Some apps send form data instead of json so we should cater for those
2119 if (!xs_is_null(payload)) { 2137 if (!xs_is_null(payload)) {
2120 xs *upl = xs_url_dec(payload); 2138 xs *upl = xs_url_dec(payload);
2121 args = xs_url_vars(upl); 2139 args = xs_url_vars(upl);
@@ -2241,7 +2259,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2241 strcmp(visibility, "public") == 0 ? 0 : 1); 2259 strcmp(visibility, "public") == 0 ? 0 : 1);
2242 2260
2243 if (!xs_is_null(summary) && *summary) { 2261 if (!xs_is_null(summary) && *summary) {
2244 msg = xs_dict_set(msg, "sensitive", xs_stock_true); 2262 msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
2245 msg = xs_dict_set(msg, "summary", summary); 2263 msg = xs_dict_set(msg, "summary", summary);
2246 } 2264 }
2247 2265
@@ -2298,11 +2316,13 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2298 } 2316 }
2299 else 2317 else
2300 if (strcmp(op, "unfavourite") == 0) { /** **/ 2318 if (strcmp(op, "unfavourite") == 0) { /** **/
2301 /* partial support: as the original Like message 2319 xs *n_msg = msg_repulsion(&snac, id, "Like");
2302 is not stored anywhere here, it's not possible 2320
2303 to send an Undo + Like; the only thing done here 2321 if (n_msg != NULL) {
2304 is to delete the actor from the list of likes */ 2322 enqueue_message(&snac, n_msg);
2305 object_unadmire(id, snac.actor, 1); 2323
2324 out = mastoapi_status(&snac, msg);
2325 }
2306 } 2326 }
2307 else 2327 else
2308 if (strcmp(op, "reblog") == 0) { /** **/ 2328 if (strcmp(op, "reblog") == 0) { /** **/
@@ -2317,8 +2337,13 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2317 } 2337 }
2318 else 2338 else
2319 if (strcmp(op, "unreblog") == 0) { /** **/ 2339 if (strcmp(op, "unreblog") == 0) { /** **/
2320 /* partial support: see comment in 'unfavourite' */ 2340 xs *n_msg = msg_repulsion(&snac, id, "Announce");
2321 object_unadmire(id, snac.actor, 0); 2341
2342 if (n_msg != NULL) {
2343 enqueue_message(&snac, n_msg);
2344
2345 out = mastoapi_status(&snac, msg);
2346 }
2322 } 2347 }
2323 else 2348 else
2324 if (strcmp(op, "bookmark") == 0) { /** **/ 2349 if (strcmp(op, "bookmark") == 0) { /** **/
@@ -2603,6 +2628,8 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2603 if (logged_in) 2628 if (logged_in)
2604 user_free(&snac); 2629 user_free(&snac);
2605 2630
2631 srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status));
2632
2606 return status; 2633 return status;
2607} 2634}
2608 2635
@@ -2701,7 +2728,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
2701 if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) { 2728 if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) {
2702 const char *content = xs_dict_get(args, "status"); 2729 const char *content = xs_dict_get(args, "status");
2703 xs *atls = xs_list_new(); 2730 xs *atls = xs_list_new();
2704 xs *f_content = not_really_markdown(content, &atls); 2731 xs *f_content = not_really_markdown(content, &atls, NULL);
2705 2732
2706 /* replace fields with new content */ 2733 /* replace fields with new content */
2707 msg = xs_dict_set(msg, "sourceContent", content); 2734 msg = xs_dict_set(msg, "sourceContent", content);
diff --git a/snac.h b/snac.h
index 1afbfc7..cac09a9 100644
--- a/snac.h
+++ b/snac.h
@@ -1,7 +1,7 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
3 3
4#define VERSION "2.50-dev" 4#define VERSION "2.52-dev"
5 5
6#define USER_AGENT "snac/" VERSION 6#define USER_AGENT "snac/" VERSION
7 7
@@ -110,6 +110,10 @@ int object_del(const char *id);
110int object_del_if_unref(const char *id); 110int object_del_if_unref(const char *id);
111double object_ctime_by_md5(const char *md5); 111double object_ctime_by_md5(const char *md5);
112double object_ctime(const char *id); 112double object_ctime(const char *id);
113double object_mtime_by_md5(const char *md5);
114double object_mtime(const char *id);
115void object_touch(const char *id);
116
113int object_admire(const char *id, const char *actor, int like); 117int object_admire(const char *id, const char *actor, int like);
114int object_unadmire(const char *id, const char *actor, int like); 118int object_unadmire(const char *id, const char *actor, int like);
115 119
@@ -172,6 +176,7 @@ xs_list *tag_search(char *tag, int skip, int show);
172 176
173int actor_add(const char *actor, xs_dict *msg); 177int actor_add(const char *actor, xs_dict *msg);
174int actor_get(const char *actor, xs_dict **data); 178int actor_get(const char *actor, xs_dict **data);
179int actor_get_refresh(snac *user, const char *actor, xs_dict **data);
175 180
176int static_get(snac *snac, const char *id, xs_val **data, int *size, const char *inm, xs_str **etag); 181int static_get(snac *snac, const char *id, xs_val **data, int *size, const char *inm, xs_str **etag);
177void static_put(snac *snac, const char *id, const char *data, int size); 182void static_put(snac *snac, const char *id, const char *data, int size);
@@ -204,6 +209,8 @@ int is_instance_blocked(const char *instance);
204int instance_block(const char *instance); 209int instance_block(const char *instance);
205int instance_unblock(const char *instance); 210int instance_unblock(const char *instance);
206 211
212int content_check(const char *file, const xs_dict *msg);
213
207void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries); 214void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries);
208void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries); 215void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries);
209void enqueue_output_raw(const char *keyid, const char *seckey, 216void enqueue_output_raw(const char *keyid, const char *seckey,
@@ -216,6 +223,7 @@ void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_t
216void enqueue_message(snac *snac, const xs_dict *msg); 223void enqueue_message(snac *snac, const xs_dict *msg);
217void enqueue_close_question(snac *user, const char *id, int end_secs); 224void enqueue_close_question(snac *user, const char *id, int end_secs);
218void enqueue_verify_links(snac *user); 225void enqueue_verify_links(snac *user);
226void enqueue_actor_refresh(snac *user, const char *actor);
219void enqueue_request_replies(snac *user, const char *id); 227void enqueue_request_replies(snac *user, const char *id);
220int was_question_voted(snac *user, const char *id); 228int was_question_voted(snac *user, const char *id);
221 229
@@ -256,6 +264,7 @@ char *get_atto(const xs_dict *msg);
256xs_list *get_attachments(const xs_dict *msg); 264xs_list *get_attachments(const xs_dict *msg);
257 265
258xs_dict *msg_admiration(snac *snac, char *object, char *type); 266xs_dict *msg_admiration(snac *snac, char *object, char *type);
267xs_dict *msg_repulsion(snac *user, char *id, char *type);
259xs_dict *msg_create(snac *snac, const xs_dict *object); 268xs_dict *msg_create(snac *snac, const xs_dict *object);
260xs_dict *msg_follow(snac *snac, const char *actor); 269xs_dict *msg_follow(snac *snac, const char *actor);
261 270
@@ -296,7 +305,8 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
296 char *payload, int p_size, 305 char *payload, int p_size,
297 char **body, int *b_size, char **ctype); 306 char **body, int *b_size, char **ctype);
298 307
299xs_str *not_really_markdown(const char *content, xs_list **attach); 308xs_dict *emojis(void);
309xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag);
300xs_str *sanitize(const char *content); 310xs_str *sanitize(const char *content);
301xs_str *encode_html(const char *str); 311xs_str *encode_html(const char *str);
302 312
diff --git a/xs.h b/xs.h
index 85464db..bab315a 100644
--- a/xs.h
+++ b/xs.h
@@ -45,6 +45,10 @@ typedef char xs_data;
45/* not really all, just very much */ 45/* not really all, just very much */
46#define XS_ALL 0xfffffff 46#define XS_ALL 0xfffffff
47 47
48#ifndef xs_countof
49#define xs_countof(a) (sizeof((a)) / sizeof((*a)))
50#endif
51
48void *xs_free(void *ptr); 52void *xs_free(void *ptr);
49void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func); 53void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func);
50#define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __FUNCTION__) 54#define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __FUNCTION__)
@@ -61,6 +65,7 @@ xs_val *xs_collapse(xs_val *data, int offset, int size);
61xs_val *xs_insert_m(xs_val *data, int offset, const char *mem, int size); 65xs_val *xs_insert_m(xs_val *data, int offset, const char *mem, int size);
62#define xs_insert(data, offset, data2) xs_insert_m(data, offset, data2, xs_size(data2)) 66#define xs_insert(data, offset, data2) xs_insert_m(data, offset, data2, xs_size(data2))
63#define xs_append_m(data, mem, size) xs_insert_m(data, xs_size(data) - 1, mem, size) 67#define xs_append_m(data, mem, size) xs_insert_m(data, xs_size(data) - 1, mem, size)
68xs_val *xs_stock(int type);
64 69
65xs_str *xs_str_new(const char *str); 70xs_str *xs_str_new(const char *str);
66xs_str *xs_str_new_sz(const char *mem, int sz); 71xs_str *xs_str_new_sz(const char *mem, int sz);
@@ -137,34 +142,11 @@ unsigned int xs_hash_func(const char *data, int size);
137#define XS_ASSERT_TYPE_NULL(v, t) (void)(0) 142#define XS_ASSERT_TYPE_NULL(v, t) (void)(0)
138#endif 143#endif
139 144
140extern xs_val xs_stock_null[];
141extern xs_val xs_stock_true[];
142extern xs_val xs_stock_false[];
143extern xs_val xs_stock_0[];
144extern xs_val xs_stock_1[];
145extern xs_val xs_stock_list[];
146extern xs_val xs_stock_dict[];
147
148#define xs_return(v) xs_val *__r = v; v = NULL; return __r 145#define xs_return(v) xs_val *__r = v; v = NULL; return __r
149 146
150 147
151#ifdef XS_IMPLEMENTATION 148#ifdef XS_IMPLEMENTATION
152 149
153xs_val xs_stock_null[] = { XSTYPE_NULL };
154xs_val xs_stock_true[] = { XSTYPE_TRUE };
155xs_val xs_stock_false[] = { XSTYPE_FALSE };
156xs_val xs_stock_0[] = { XSTYPE_NUMBER, '0', '\0' };
157xs_val xs_stock_1[] = { XSTYPE_NUMBER, '1', '\0' };
158
159#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
160xs_val xs_stock_list[] = { XSTYPE_LIST, 0, 0, 0, 1 + _XS_TYPE_SIZE + 1, XSTYPE_EOM };
161xs_val xs_stock_dict[] = { XSTYPE_DICT, 0, 0, 0, 1 + _XS_TYPE_SIZE + 1, XSTYPE_EOM };
162#else
163xs_val xs_stock_list[] = { XSTYPE_LIST, 1 + _XS_TYPE_SIZE + 1, 0, 0, 0, XSTYPE_EOM };
164xs_val xs_stock_dict[] = { XSTYPE_DICT, 1 + _XS_TYPE_SIZE + 1, 0, 0, 0, XSTYPE_EOM };
165#endif
166
167
168void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func) 150void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func)
169{ 151{
170 xs_val *ndata = realloc(ptr, size); 152 xs_val *ndata = realloc(ptr, size);
@@ -369,10 +351,14 @@ int xs_cmp(const xs_val *v1, const xs_val *v2)
369xs_val *xs_dup(const xs_val *data) 351xs_val *xs_dup(const xs_val *data)
370/* creates a duplicate of data */ 352/* creates a duplicate of data */
371{ 353{
372 int sz = xs_size(data); 354 xs_val *s = NULL;
373 xs_val *s = xs_realloc(NULL, _xs_blk_size(sz)); 355
356 if (data) {
357 int sz = xs_size(data);
358 s = xs_realloc(NULL, _xs_blk_size(sz));
374 359
375 memcpy(s, data, sz); 360 memcpy(s, data, sz);
361 }
376 362
377 return s; 363 return s;
378} 364}
@@ -437,6 +423,39 @@ xs_val *xs_insert_m(xs_val *data, int offset, const char *mem, int size)
437} 423}
438 424
439 425
426xs_val *xs_stock(int type)
427/* returns stock values */
428{
429 static xs_val stock_null[] = { XSTYPE_NULL };
430 static xs_val stock_true[] = { XSTYPE_TRUE };
431 static xs_val stock_false[] = { XSTYPE_FALSE };
432 static xs_val stock_0[] = { XSTYPE_NUMBER, '0', '\0' };
433 static xs_val stock_1[] = { XSTYPE_NUMBER, '1', '\0' };
434 static xs_list *stock_list = NULL;
435 static xs_dict *stock_dict = NULL;
436
437 switch (type) {
438 case 0: return stock_0;
439 case 1: return stock_1;
440 case XSTYPE_NULL: return stock_null;
441 case XSTYPE_TRUE: return stock_true;
442 case XSTYPE_FALSE: return stock_false;
443
444 case XSTYPE_LIST:
445 if (stock_list == NULL)
446 stock_list = xs_list_new();
447 return stock_list;
448
449 case XSTYPE_DICT:
450 if (stock_dict == NULL)
451 stock_dict = xs_dict_new();
452 return stock_dict;
453 }
454
455 return NULL;
456}
457
458
440/** strings **/ 459/** strings **/
441 460
442xs_str *xs_str_new(const char *str) 461xs_str *xs_str_new(const char *str)
@@ -647,10 +666,14 @@ xs_str *xs_tolower_i(xs_str *str)
647xs_list *xs_list_new(void) 666xs_list *xs_list_new(void)
648/* creates a new list */ 667/* creates a new list */
649{ 668{
650 return memcpy( 669 int sz = 1 + _XS_TYPE_SIZE + 1;
651 xs_realloc(NULL, _xs_blk_size(sizeof(xs_stock_list))), 670 xs_list *l = xs_realloc(NULL, sz);
652 xs_stock_list, sizeof(xs_stock_list) 671 memset(l, '\0', sz);
653 ); 672
673 l[0] = XSTYPE_LIST;
674 _xs_put_size(&l[1], sz);
675
676 return l;
654} 677}
655 678
656 679
@@ -660,8 +683,8 @@ xs_list *_xs_list_write_litem(xs_list *list, int offset, const char *mem, int ds
660 XS_ASSERT_TYPE(list, XSTYPE_LIST); 683 XS_ASSERT_TYPE(list, XSTYPE_LIST);
661 684
662 if (mem == NULL) { 685 if (mem == NULL) {
663 mem = xs_stock_null; 686 mem = xs_stock(XSTYPE_NULL);
664 dsz = sizeof(xs_stock_null); 687 dsz = xs_size(mem);
665 } 688 }
666 689
667 list = xs_expand(list, offset, dsz + 1); 690 list = xs_expand(list, offset, dsz + 1);
@@ -947,10 +970,14 @@ xs_list *xs_list_cat(xs_list *l1, const xs_list *l2)
947xs_dict *xs_dict_new(void) 970xs_dict *xs_dict_new(void)
948/* creates a new dict */ 971/* creates a new dict */
949{ 972{
950 return memcpy( 973 int sz = 1 + _XS_TYPE_SIZE + 1;
951 xs_realloc(NULL, _xs_blk_size(sizeof(xs_stock_dict))), 974 xs_dict *d = xs_realloc(NULL, sz);
952 xs_stock_dict, sizeof(xs_stock_dict) 975 memset(d, '\0', sz);
953 ); 976
977 d[0] = XSTYPE_DICT;
978 _xs_put_size(&d[1], sz);
979
980 return d;
954} 981}
955 982
956 983
@@ -962,8 +989,8 @@ xs_dict *_xs_dict_write_ditem(xs_dict *dict, int offset, const xs_str *key,
962 XS_ASSERT_TYPE(key, XSTYPE_STRING); 989 XS_ASSERT_TYPE(key, XSTYPE_STRING);
963 990
964 if (data == NULL) { 991 if (data == NULL) {
965 data = xs_stock_null; 992 data = xs_stock(XSTYPE_NULL);
966 dsz = sizeof(xs_stock_null); 993 dsz = xs_size(data);
967 } 994 }
968 995
969 int ksz = xs_size(key); 996 int ksz = xs_size(key);
diff --git a/xs_json.h b/xs_json.h
index d656b15..6706d7e 100644
--- a/xs_json.h
+++ b/xs_json.h
@@ -11,8 +11,10 @@ xs_val *xs_json_load(FILE *f);
11xs_val *xs_json_loads(const xs_str *json); 11xs_val *xs_json_loads(const xs_str *json);
12 12
13xstype xs_json_load_type(FILE *f); 13xstype xs_json_load_type(FILE *f);
14int xs_json_load_array_iter(FILE *f, xs_val **value, int *c); 14int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c);
15int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c); 15int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c);
16xs_list *xs_json_load_array(FILE *f);
17xs_dict *xs_json_load_object(FILE *f);
16 18
17 19
18#ifdef XS_IMPLEMENTATION 20#ifdef XS_IMPLEMENTATION
@@ -324,10 +326,9 @@ static xs_val *_xs_json_load_lexer(FILE *f, js_type *t)
324} 326}
325 327
326 328
327static xs_list *_xs_json_load_array(FILE *f); 329int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c)
328static xs_dict *_xs_json_load_object(FILE *f); 330/* loads the next scalar value from the JSON stream */
329 331/* if the value ahead is compound, value is NULL and pt is set */
330int xs_json_load_array_iter(FILE *f, xs_val **value, int *c)
331{ 332{
332 js_type t; 333 js_type t;
333 334
@@ -346,14 +347,16 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, int *c)
346 return -1; 347 return -1;
347 } 348 }
348 349
349 if (t == JS_OBRACK) 350 if (*value == NULL) {
350 *value = _xs_json_load_array(f); 351 /* possible compound type ahead */
351 else 352 if (t == JS_OBRACK)
352 if (t == JS_OCURLY) 353 *pt = XSTYPE_LIST;
353 *value = _xs_json_load_object(f); 354 else
354 355 if (t == JS_OCURLY)
355 if (*value == NULL) 356 *pt = XSTYPE_DICT;
356 return -1; 357 else
358 return -1;
359 }
357 360
358 *c = *c + 1; 361 *c = *c + 1;
359 362
@@ -361,21 +364,38 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, int *c)
361} 364}
362 365
363 366
364static xs_list *_xs_json_load_array(FILE *f) 367xs_list *xs_json_load_array(FILE *f)
365/* parses a JSON array */ 368/* loads a full JSON array (after the initial OBRACK) */
366{ 369{
370 xstype t;
367 xs_list *l = xs_list_new(); 371 xs_list *l = xs_list_new();
368 int c = 0; 372 int c = 0;
369 373
370 for (;;) { 374 for (;;) {
371 xs *v = NULL; 375 xs *v = NULL;
372 int r = xs_json_load_array_iter(f, &v, &c); 376 int r = xs_json_load_array_iter(f, &v, &t, &c);
373 377
374 if (r == -1) 378 if (r == -1)
375 l = xs_free(l); 379 l = xs_free(l);
376 380
377 if (r == 1) 381 if (r == 1) {
382 /* partial load? */
383 if (v == NULL) {
384 if (t == XSTYPE_LIST)
385 v = xs_json_load_array(f);
386 else
387 if (t == XSTYPE_DICT)
388 v = xs_json_load_object(f);
389 }
390
391 /* still null? fail */
392 if (v == NULL) {
393 l = xs_free(l);
394 break;
395 }
396
378 l = xs_list_append(l, v); 397 l = xs_list_append(l, v);
398 }
379 else 399 else
380 break; 400 break;
381 } 401 }
@@ -384,7 +404,9 @@ static xs_list *_xs_json_load_array(FILE *f)
384} 404}
385 405
386 406
387int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c) 407int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c)
408/* loads the next key and scalar value from the JSON stream */
409/* if the value ahead is compound, value is NULL and pt is set */
388{ 410{
389 js_type t; 411 js_type t;
390 412
@@ -413,14 +435,16 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c)
413 435
414 *value = _xs_json_load_lexer(f, &t); 436 *value = _xs_json_load_lexer(f, &t);
415 437
416 if (t == JS_OBRACK) 438 if (*value == NULL) {
417 *value = _xs_json_load_array(f); 439 /* possible complex type ahead */
418 else 440 if (t == JS_OBRACK)
419 if (t == JS_OCURLY) 441 *pt = XSTYPE_LIST;
420 *value = _xs_json_load_object(f); 442 else
421 443 if (t == JS_OCURLY)
422 if (*value == NULL) 444 *pt = XSTYPE_DICT;
423 return -1; 445 else
446 return -1;
447 }
424 448
425 *c = *c + 1; 449 *c = *c + 1;
426 450
@@ -428,22 +452,39 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c)
428} 452}
429 453
430 454
431static xs_dict *_xs_json_load_object(FILE *f) 455xs_dict *xs_json_load_object(FILE *f)
432/* parses a JSON object */ 456/* loads a full JSON object (after the initial OCURLY) */
433{ 457{
458 xstype t;
434 xs_dict *d = xs_dict_new(); 459 xs_dict *d = xs_dict_new();
435 int c = 0; 460 int c = 0;
436 461
437 for (;;) { 462 for (;;) {
438 xs *k = NULL; 463 xs *k = NULL;
439 xs *v = NULL; 464 xs *v = NULL;
440 int r = xs_json_load_object_iter(f, &k, &v, &c); 465 int r = xs_json_load_object_iter(f, &k, &v, &t, &c);
441 466
442 if (r == -1) 467 if (r == -1)
443 d = xs_free(d); 468 d = xs_free(d);
444 469
445 if (r == 1) 470 if (r == 1) {
471 /* partial load? */
472 if (v == NULL) {
473 if (t == XSTYPE_LIST)
474 v = xs_json_load_array(f);
475 else
476 if (t == XSTYPE_DICT)
477 v = xs_json_load_object(f);
478 }
479
480 /* still null? fail */
481 if (v == NULL) {
482 d = xs_free(d);
483 break;
484 }
485
446 d = xs_dict_append(d, k, v); 486 d = xs_dict_append(d, k, v);
487 }
447 else 488 else
448 break; 489 break;
449 } 490 }
@@ -492,10 +533,10 @@ xs_val *xs_json_load(FILE *f)
492 xstype t = xs_json_load_type(f); 533 xstype t = xs_json_load_type(f);
493 534
494 if (t == XSTYPE_LIST) 535 if (t == XSTYPE_LIST)
495 v = _xs_json_load_array(f); 536 v = xs_json_load_array(f);
496 else 537 else
497 if (t == XSTYPE_DICT) 538 if (t == XSTYPE_DICT)
498 v = _xs_json_load_object(f); 539 v = xs_json_load_object(f);
499 540
500 return v; 541 return v;
501} 542}
diff --git a/xs_mime.h b/xs_mime.h
index 84af49c..853b092 100644
--- a/xs_mime.h
+++ b/xs_mime.h
@@ -55,19 +55,23 @@ const char *xs_mime_by_ext(const char *file)
55 const char *ext = strrchr(file, '.'); 55 const char *ext = strrchr(file, '.');
56 56
57 if (ext) { 57 if (ext) {
58 const char **p = xs_mime_types; 58 xs *uext = xs_tolower_i(xs_dup(ext + 1));
59 xs *uext = xs_tolower_i(xs_dup(ext + 1)); 59 int b = 0;
60 int t = xs_countof(xs_mime_types) / 2 - 2;
60 61
61 while (*p) { 62 while (t >= b) {
62 int c; 63 int n = (b + t) / 2;
64 const char *p = xs_mime_types[n * 2];
63 65
64 if ((c = strcmp(*p, uext)) == 0) 66 int c = strcmp(uext, p);
65 return p[1]; 67
68 if (c < 0)
69 t = n - 1;
66 else 70 else
67 if (c > 0) 71 if (c > 0)
68 break; 72 b = n + 1;
69 73 else
70 p += 2; 74 return xs_mime_types[(n * 2) + 1];
71 } 75 }
72 } 76 }
73 77
diff --git a/xs_unicode.h b/xs_unicode.h
index 47e1101..6654da4 100644
--- a/xs_unicode.h
+++ b/xs_unicode.h
@@ -27,8 +27,8 @@
27 27
28#ifdef XS_IMPLEMENTATION 28#ifdef XS_IMPLEMENTATION
29 29
30#ifndef countof 30#ifndef xs_countof
31#define countof(a) (sizeof((a)) / sizeof((*a))) 31#define xs_countof(a) (sizeof((a)) / sizeof((*a)))
32#endif 32#endif
33 33
34int _xs_utf8_enc(char buf[4], unsigned int cpoint) 34int _xs_utf8_enc(char buf[4], unsigned int cpoint)
@@ -125,7 +125,7 @@ int xs_unicode_width(unsigned int cpoint)
125/* returns the width in columns of a Unicode codepoint (somewhat simplified) */ 125/* returns the width in columns of a Unicode codepoint (somewhat simplified) */
126{ 126{
127 int b = 0; 127 int b = 0;
128 int t = countof(xs_unicode_width_table) / 3 - 1; 128 int t = xs_countof(xs_unicode_width_table) / 3 - 1;
129 129
130 while (t >= b) { 130 while (t >= b) {
131 int n = (b + t) / 2; 131 int n = (b + t) / 2;
@@ -193,7 +193,7 @@ unsigned int *_xs_unicode_upper_search(unsigned int cpoint)
193/* searches for an uppercase codepoint in the case fold table */ 193/* searches for an uppercase codepoint in the case fold table */
194{ 194{
195 int b = 0; 195 int b = 0;
196 int t = countof(xs_unicode_case_fold_table) / 2 + 1; 196 int t = xs_countof(xs_unicode_case_fold_table) / 2 + 1;
197 197
198 while (t >= b) { 198 while (t >= b) {
199 int n = (b + t) / 2; 199 int n = (b + t) / 2;
@@ -216,7 +216,7 @@ unsigned int *_xs_unicode_lower_search(unsigned int cpoint)
216/* searches for a lowercase codepoint in the case fold table */ 216/* searches for a lowercase codepoint in the case fold table */
217{ 217{
218 unsigned int *p = xs_unicode_case_fold_table; 218 unsigned int *p = xs_unicode_case_fold_table;
219 unsigned int *e = p + countof(xs_unicode_case_fold_table); 219 unsigned int *e = p + xs_countof(xs_unicode_case_fold_table);
220 220
221 while (p < e) { 221 while (p < e) {
222 if (cpoint == p[1]) 222 if (cpoint == p[1])
@@ -251,7 +251,7 @@ int xs_unicode_nfd(unsigned int cpoint, unsigned int *base, unsigned int *diac)
251/* applies unicode Normalization Form D */ 251/* applies unicode Normalization Form D */
252{ 252{
253 int b = 0; 253 int b = 0;
254 int t = countof(xs_unicode_nfd_table) / 3 - 1; 254 int t = xs_countof(xs_unicode_nfd_table) / 3 - 1;
255 255
256 while (t >= b) { 256 while (t >= b) {
257 int n = (b + t) / 2; 257 int n = (b + t) / 2;
@@ -279,7 +279,7 @@ int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint)
279/* applies unicode Normalization Form C */ 279/* applies unicode Normalization Form C */
280{ 280{
281 unsigned int *p = xs_unicode_nfd_table; 281 unsigned int *p = xs_unicode_nfd_table;
282 unsigned int *e = p + countof(xs_unicode_nfd_table); 282 unsigned int *e = p + xs_countof(xs_unicode_nfd_table);
283 283
284 while (p < e) { 284 while (p < e) {
285 if (p[1] == base && p[2] == diac) { 285 if (p[1] == base && p[2] == diac) {
@@ -298,7 +298,7 @@ int xs_unicode_is_alpha(unsigned int cpoint)
298/* checks if a codepoint is an alpha (i.e. a letter) */ 298/* checks if a codepoint is an alpha (i.e. a letter) */
299{ 299{
300 int b = 0; 300 int b = 0;
301 int t = countof(xs_unicode_alpha_table) / 2 - 1; 301 int t = xs_countof(xs_unicode_alpha_table) / 2 - 1;
302 302
303 while (t >= b) { 303 while (t >= b) {
304 int n = (b + t) / 2; 304 int n = (b + t) / 2;
diff --git a/xs_url.h b/xs_url.h
index f335709..6c9c8b5 100644
--- a/xs_url.h
+++ b/xs_url.h
@@ -56,7 +56,7 @@ xs_dict *xs_url_vars(const char *str)
56 56
57 l = args; 57 l = args;
58 while (xs_list_iter(&l, &v)) { 58 while (xs_list_iter(&l, &v)) {
59 xs *kv = xs_split_n(v, "=", 2); 59 xs *kv = xs_split_n(v, "=", 1);
60 60
61 if (xs_list_len(kv) == 2) { 61 if (xs_list_len(kv) == 2) {
62 const char *key = xs_list_get(kv, 0); 62 const char *key = xs_list_get(kv, 0);
diff --git a/xs_version.h b/xs_version.h
index 50dcb5e..f655735 100644
--- a/xs_version.h
+++ b/xs_version.h
@@ -1 +1 @@
/* f46d5b29627b20a6e9ec4ef60c01df1d2d778520 2024-03-09T08:26:31+01:00 */ /* f712d1336ef427c3b56305364b2687578537543f 2024-04-14T19:11:53+02:00 */