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:
activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h
data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
- xs_set.h xs_time.h snac.h
-format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h snac.h
+ xs_set.h xs_time.h xs_regex.h snac.h
+format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
+ xs_time.h snac.h
html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
xs_time.h xs_mime.h xs_match.h xs_html.h snac.h
http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
snac.h
httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_httpd.h xs_mime.h \
xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h
-main.o: main.c xs.h xs_io.h xs_json.h xs_time.h snac.h xs_html.h
+main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h
mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \
snac.h
diff --git a/Makefile.NetBSD b/Makefile.NetBSD
index 5ab361f..67c77a5 100644
--- a/Makefile.NetBSD
+++ b/Makefile.NetBSD
@@ -1,6 +1,7 @@
PREFIX=/usr/pkg
PREFIX_MAN=$(PREFIX)/man
CFLAGS?=-g -Wall -Wextra
+LDFLAGS=-lrt
all: snac
@@ -37,15 +38,16 @@ uninstall:
activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h
data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
- xs_set.h xs_time.h snac.h
-format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h snac.h
+ xs_set.h xs_time.h xs_regex.h snac.h
+format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
+ xs_time.h snac.h
html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
xs_time.h xs_mime.h xs_match.h xs_html.h snac.h
http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
snac.h
httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_httpd.h xs_mime.h \
xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h
-main.o: main.c xs.h xs_io.h xs_json.h xs_time.h snac.h xs_html.h
+main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h
mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \
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:
- [Online snac manuals (user, administrator and data formats)](https://comam.es/snac-doc/).
- [How to run your own ActivityPub server on OpenBSD via snac (by Jordan Reger)](https://man.sr.ht/~jordanreger/activitypub-server-on-openbsd/).
- [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/).
+- [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html).
+- [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac).
## Incredibly awesome CSS themes for snac
+- [A compilation of themes for snac (by Ворон)](https://codeberg.org/voron/snac-style).
- [A cool, elegant theme (by Haijo7)](https://codeberg.org/Haijo7/snac-custom-css).
-- [A light, lean theme (by Ворон)](https://codeberg.org/voron/snac-style).
- [A terminal-like theme (by Tetra)](https://codeberg.org/ERROR404NULLNOTFOUND/snac-terminal-theme).
## 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 @@
# Release Notes
+## 2.52
+
+Posts that were liked or boosted can now be unliked and unboosted.
+
+Added a header to avoid over-zealous caching in some browsers (contributed by louis77).
+
+## 2.51
+
+Support 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).
+
+Fixed a bug that caused some notifications to be lost when coming from a user in the same instance.
+
+Added an additional check for blocked instances (sometimes, posts from blocked sites that were ancestors of legit posts were 'leaking' into the timeline).
+
+On 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.
+
+## 2.50
+
+Incoming 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)`).
+
+Improved page position after hitting the `Hide` or `MUTE` buttons (for most cases).
+
+Use a shorter maximum conversation thread level (also, this maximum value is now configurable at compilation level with the `MAX_CONVERSATION_LEVELS` define).
+
+Fixed a bug where editing a post made the attached media or video to be lost.
+
+The way of refreshing remote actor data has been improved.
+
+Posting from the command-line now allows attachments.
+
+Added defines for time to enable MacOS builds (contributed by andypiper).
+
## 2.49
Mastodon 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.
Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't.
-Post edits should preserve the image and the image description somewhat.
-
Mastodon API: fix whatever the fuck is making the official app and Megalodon to crash.
Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721
## Wishlist
+Implement `Group`-like accounts (i.e. an actor that boosts to their followers all posts that mention it).
+
Integrate "Ability to federate with hidden networks" see https://codeberg.org/grunfink/snac2/issues/93
Integrate "Added handling for International Domain Names" PR https://codeberg.org/grunfink/snac2/pulls/104
-Consider discarding posts by content using string or regex to mitigate spam.
+Consider adding Mastodon import functionality (for following_accounts.csv and outbox.json).
Consider adding milter-like support to reject posts to mitigate spam.
@@ -26,7 +26,7 @@ Do something about Akkoma and Misskey's quoted replies (they use the `quoteUrl`
Add more CSS classes according to https://comam.es/snac/grunfink/p/1705598619.090050
-Add support for /share?text=tt&website=url (whatever it is).
+Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details).
Add support for /authorize_interaction (whatever it is).
@@ -307,3 +307,7 @@ Add support for rel="me" links, see https://codeberg.org/grunfink/snac2/issues/1
Hide followers-only replies to unknown accounts, see https://codeberg.org/grunfink/snac2/issues/123 (2024-02-22T12:40:58+0100).
Consider 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).
+
+Consider discarding posts by content using string or regex to mitigate spam (2024-03-14T10:40:14+0100).
+
+Post 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)
*data = NULL;
/* get from disk first */
- status = actor_get(actor, data);
+ status = actor_get_refresh(user, actor, data);
- if (status != 200) {
- /* actor data non-existent or stale: get from the net */
+ if (!valid_status(status)) {
+ /* actor data non-existent: get from the net */
status = activitypub_request(user, actor, &payload);
if (valid_status(status)) {
@@ -149,8 +149,6 @@ int actor_request(snac *user, const char *actor, xs_dict **data)
if (valid_status(status) && data && *data)
inbox_add_by_actor(*data);
}
- else
- srv_debug(2, xs_fmt("NOT collected"));
return status;
}
@@ -313,6 +311,12 @@ int timeline_request(snac *snac, char **id, xs_str **wrk, int level)
if (level < MAX_CONVERSATION_LEVELS && !xs_is_null(*id)) {
xs *msg = NULL;
+ /* from a blocked instance? discard and break */
+ if (is_instance_blocked(*id)) {
+ snac_debug(snac, 1, xs_fmt("timeline_request blocked instance %s", *id));
+ return status;
+ }
+
/* is the object already there? */
if (!valid_status(object_get(*id, &msg))) {
/* no; download it */
@@ -354,18 +358,22 @@ int timeline_request(snac *snac, char **id, xs_str **wrk, int level)
if (xs_match(type, "Note|Page|Article|Video")) {
const char *actor = get_atto(object);
- /* request (and drop) the actor for this entry */
- if (!xs_is_null(actor))
- actor_request(snac, actor, NULL);
+ if (content_check("filter_reject.txt", object))
+ snac_log(snac, xs_fmt("timeline_request rejected by content %s", nid));
+ else {
+ /* request (and drop) the actor for this entry */
+ if (!xs_is_null(actor))
+ actor_request(snac, actor, NULL);
- /* does it have an ancestor? */
- char *in_reply_to = xs_dict_get(object, "inReplyTo");
+ /* does it have an ancestor? */
+ char *in_reply_to = xs_dict_get(object, "inReplyTo");
- /* store */
- timeline_add(snac, nid, object);
+ /* store */
+ timeline_add(snac, nid, object);
- /* recurse! */
- timeline_request(snac, &in_reply_to, NULL, level + 1);
+ /* recurse! */
+ timeline_request(snac, &in_reply_to, NULL, level + 1);
+ }
}
}
}
@@ -623,6 +631,12 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
const char *type = xs_dict_get(c_msg, "type");
const char *actor = xs_dict_get(c_msg, "actor");
+ if (strcmp(actor, snac->actor) == 0) {
+ /* message by myself? (most probably via the shared-inbox) reject */
+ snac_debug(snac, 1, xs_fmt("ignoring message by myself"));
+ return 0;
+ }
+
if (xs_match(type, "Like|Announce")) {
const char *object = xs_dict_get(c_msg, "object");
@@ -657,6 +671,12 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
return !xs_is_null(object) && strcmp(snac->actor, object) == 0;
}
+ /* only accept Ping directed to us */
+ if (xs_match(type, "Ping")) {
+ char *dest = xs_dict_get(c_msg, "to");
+ return !xs_is_null(dest) && strcmp(snac->actor, dest) == 0;
+ }
+
/* if it's not a Create or Update, allow as is */
if (!xs_match(type, "Create|Update")) {
return 1;
@@ -1072,7 +1092,7 @@ xs_dict *msg_collection(snac *snac, char *id)
msg = xs_dict_append(msg, "attributedTo", snac->actor);
msg = xs_dict_append(msg, "orderedItems", ol);
- msg = xs_dict_append(msg, "totalItems", xs_stock_0);
+ msg = xs_dict_append(msg, "totalItems", xs_stock(0));
return msg;
}
@@ -1129,8 +1149,10 @@ xs_dict *msg_admiration(snac *snac, char *object, char *type)
if (valid_status(object_get(object, &a_msg))) {
xs *rcpts = xs_list_new();
+ xs *o_md5 = xs_md5_hex(object, strlen(object));
+ xs *id = xs_fmt("%s/%s/%s", snac->actor, *type == 'L' ? "l" : "a", o_md5);
- msg = msg_base(snac, type, "@dummy", snac->actor, "@now", object);
+ msg = msg_base(snac, type, id, snac->actor, "@now", object);
if (is_msg_public(a_msg))
rcpts = xs_list_append(rcpts, public_address);
@@ -1146,6 +1168,33 @@ xs_dict *msg_admiration(snac *snac, char *object, char *type)
}
+xs_dict *msg_repulsion(snac *user, char *id, char *type)
+/* creates an Undo + admiration message */
+{
+ xs *a_msg = NULL;
+ xs_dict *msg = NULL;
+
+ if (valid_status(object_get(id, &a_msg))) {
+ /* create a clone of the original admiration message */
+ xs *object = msg_admiration(user, id, type);
+
+ /* delete the published date */
+ object = xs_dict_del(object, "published");
+
+ /* create an undo message for this object */
+ msg = msg_undo(user, object);
+
+ /* copy the 'to' field */
+ msg = xs_dict_set(msg, "to", xs_dict_get(object, "to"));
+ }
+
+ /* now we despise this */
+ object_unadmire(id, user->actor, *type == 'L' ? 1 : 0);
+
+ return msg;
+}
+
+
xs_dict *msg_actor(snac *snac)
/* create a Person message for this actor */
{
@@ -1170,7 +1219,7 @@ xs_dict *msg_actor(snac *snac)
msg = xs_dict_set(msg, "preferredUsername", snac->uid);
msg = xs_dict_set(msg, "published", xs_dict_get(snac->config, "published"));
- xs *f_bio_2 = not_really_markdown(xs_dict_get(snac->config, "bio"), NULL);
+ xs *f_bio_2 = not_really_markdown(xs_dict_get(snac->config, "bio"), NULL, NULL);
f_bio = process_tags(snac, f_bio_2, &tags);
msg = xs_dict_set(msg, "summary", f_bio);
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,
}
/* format the content */
- fc2 = not_really_markdown(content, &atls);
+ fc2 = not_really_markdown(content, &atls, &tag);
if (in_reply_to != NULL && *in_reply_to) {
xs *p_msg = NULL;
@@ -1556,6 +1605,7 @@ xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
}
if (xs_set_add(&seen, v2) == 1) {
+ d = xs_dict_append(d, "type", "Note");
d = xs_dict_append(d, "name", v2);
d = xs_dict_append(d, "replies", replies);
o = xs_list_append(o, d);
@@ -1608,7 +1658,7 @@ int update_question(snac *user, const char *id)
const char *name = xs_dict_get(v, "name");
if (name) {
lopts = xs_list_append(lopts, name);
- rcnt = xs_dict_set(rcnt, name, xs_stock_0);
+ rcnt = xs_dict_set(rcnt, name, xs_stock(0));
}
}
@@ -1891,6 +1941,8 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
}
else
if (strcmp(type, "Undo") == 0) { /** **/
+ char *id = xs_dict_get(object, "object");
+
if (xs_type(object) != XSTYPE_DICT)
utype = "Follow";
@@ -1903,6 +1955,23 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
snac_log(snac, xs_fmt("error deleting follower %s", actor));
}
else
+ if (strcmp(utype, "Like") == 0) { /** **/
+ int status = object_unadmire(id, actor, 1);
+
+ snac_log(snac, xs_fmt("Unlike for %s %d", id, status));
+ }
+ else
+ if (strcmp(utype, "Announce") == 0) { /** **/
+ int status = 200;
+
+ /* commented out: if a followed user boosts something that
+ is requested and then unboosts, the post remains here,
+ but with no apparent reason, and that is confusing */
+ //status = object_unadmire(id, actor, 0);
+
+ snac_log(snac, xs_fmt("Unboost for %s %d", id, status));
+ }
+ else
snac_debug(snac, 1, xs_fmt("ignored 'Undo' for object type '%s'", utype));
}
else
@@ -1912,7 +1981,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
return 1;
}
- if (strcmp(utype, "Note") == 0) { /** **/
+ if (xs_match(utype, "Note|Article")) { /** **/
char *id = xs_dict_get(object, "id");
char *in_reply_to = xs_dict_get(object, "inReplyTo");
xs *wrk = NULL;
@@ -1921,10 +1990,15 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
snac_debug(snac, 0, xs_fmt("dropped reply %s to hidden post %s", id, in_reply_to));
}
else {
+ if (content_check("filter_reject.txt", object)) {
+ snac_log(snac, xs_fmt("rejected by content %s", id));
+ return 1;
+ }
+
timeline_request(snac, &in_reply_to, &wrk, 0);
if (timeline_add(snac, id, object)) {
- snac_log(snac, xs_fmt("new 'Note' %s %s", actor, id));
+ snac_log(snac, xs_fmt("new '%s' %s %s", utype, actor, id));
do_notify = 1;
}
@@ -1988,12 +2062,12 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
if (xs_type(object) == XSTYPE_DICT)
object = xs_dict_get(object, "id");
- if (timeline_admire(snac, object, actor, 1) == 201) {
+ if (timeline_admire(snac, object, actor, 1) == 201)
snac_log(snac, xs_fmt("new 'Like' %s %s", actor, object));
- do_notify = 1;
- }
else
snac_log(snac, xs_fmt("repeated 'Like' from %s to %s", actor, object));
+
+ do_notify = 1;
}
else
if (strcmp(type, "Announce") == 0) { /** **/
@@ -2019,13 +2093,13 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
xs *who_o = NULL;
if (valid_status(actor_request(snac, who, &who_o))) {
- if (timeline_admire(snac, object, actor, 0) == 201) {
+ if (timeline_admire(snac, object, actor, 0) == 201)
snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
- do_notify = 1;
- }
else
snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
actor, object));
+
+ do_notify = 1;
}
else
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)
verify_links(snac);
}
else
+ if (strcmp(type, "actor_refresh") == 0) {
+ const char *actor = xs_dict_get(q_item, "actor");
+ double mtime = object_mtime(actor);
+
+ /* only refresh if it was refreshed more than an hour ago */
+ if (mtime + 3600.0 < (double) time(NULL)) {
+ xs *actor_o = NULL;
+ int status;
+
+ if (valid_status((status = activitypub_request(snac, actor, &actor_o))))
+ actor_add(actor, actor_o);
+ else
+ object_touch(actor);
+
+ snac_log(snac, xs_fmt("actor_refresh %s %d", actor, status));
+ }
+ }
+ else
snac_log(snac, xs_fmt("unexpected user q_item type '%s'", type));
}
diff --git a/data.c b/data.c
index f4cd6d6..d3045f4 100644
--- a/data.c
+++ b/data.c
@@ -9,6 +9,7 @@
#include "xs_glob.h"
#include "xs_set.h"
#include "xs_time.h"
+#include "xs_regex.h"
#include "snac.h"
@@ -116,21 +117,33 @@ int srv_open(char *basedir, int auto_upgrade)
srv_debug(1, xs_dup("OpenBSD security disabled by admin"));
}
else {
+ int smail = xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE;
+
srv_debug(1, xs_fmt("Calling unveil()"));
unveil(basedir, "rwc");
unveil("/tmp", "rwc");
- unveil("/usr/sbin/sendmail", "x");
unveil("/etc/resolv.conf", "r");
unveil("/etc/hosts", "r");
unveil("/etc/ssl/openssl.cnf", "r");
unveil("/etc/ssl/cert.pem", "r");
unveil("/usr/share/zoneinfo", "r");
+
+ if (smail)
+ unveil("/usr/sbin/sendmail", "x");
+
unveil(NULL, NULL);
srv_debug(1, xs_fmt("Calling pledge()"));
- pledge("stdio rpath wpath cpath flock inet proc exec dns fattr", NULL);
+
+ if (smail)
+ pledge("stdio rpath wpath cpath flock inet proc exec dns fattr", NULL);
+ else
+ pledge("stdio rpath wpath cpath flock inet proc dns fattr", NULL);
}
#endif /* __OpenBSD__ */
+ /* read (and drop) emojis.json, possibly creating it */
+ xs_free(emojis());
+
return ret;
}
@@ -393,7 +406,7 @@ int index_del_md5(const char *fn, const char *md5)
fclose(f);
}
else
- status = 500;
+ status = 410;
pthread_mutex_unlock(&data_mutex);
@@ -796,6 +809,30 @@ double object_ctime(const char *id)
}
+double object_mtime_by_md5(const char *md5)
+{
+ xs *fn = _object_fn_by_md5(md5, "object_mtime_by_md5");
+ return mtime(fn);
+}
+
+
+double object_mtime(const char *id)
+{
+ xs *md5 = xs_md5_hex(id, strlen(id));
+ return object_mtime_by_md5(md5);
+}
+
+
+void object_touch(const char *id)
+{
+ xs *md5 = xs_md5_hex(id, strlen(id));
+ xs *fn = _object_fn_by_md5(md5, "object_touch");
+
+ if (mtime(fn))
+ utimes(fn, NULL);
+}
+
+
xs_str *_object_index_fn(const char *id, const char *idxsfx)
/* returns the filename of an object's index */
{
@@ -880,6 +917,9 @@ int object_unadmire(const char *id, const char *actor, int like)
status = index_del(fn, actor);
+ if (valid_status(status))
+ index_gc(fn);
+
srv_debug(0,
xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
@@ -1551,7 +1591,6 @@ int actor_get(const char *actor, xs_dict **data)
else
d = xs_free(d);
-#ifdef STALE_ACTORS
xs *fn = _object_fn(actor);
double max_time;
@@ -1560,13 +1599,20 @@ int actor_get(const char *actor, xs_dict **data)
if (mtime(fn) + max_time < (double) time(NULL)) {
/* actor data exists but also stinks */
-
- /* touch the file */
- utimes(fn, NULL);
-
status = 205; /* "205: Reset Content" "110: Response Is Stale" */
}
-#endif /* STALE_ACTORS */
+
+ return status;
+}
+
+
+int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
+/* gets an actor and requests a refresh if it's stale */
+{
+ int status = actor_get(actor, data);
+
+ if (status == 205 && user && !xs_startswith(actor, srv_baseurl))
+ enqueue_actor_refresh(user, actor);
return status;
}
@@ -2007,6 +2053,47 @@ int instance_unblock(const char *instance)
}
+/** content filtering **/
+
+int content_check(const char *file, const xs_dict *msg)
+/* checks if a message's content matches any of the regexes in file */
+/* file format: one regex per line */
+{
+ xs *fn = xs_fmt("%s/%s", srv_basedir, file);
+ FILE *f;
+ int r = 0;
+ char *v = xs_dict_get(msg, "content");
+
+ if (xs_type(v) == XSTYPE_STRING && *v) {
+ if ((f = fopen(fn, "r")) != NULL) {
+ srv_debug(1, xs_fmt("content_check: loading regexes from %s", fn));
+
+ /* massage content (strip HTML tags, etc.) */
+ xs *c = xs_regex_replace(v, "<[^>]+>", " ");
+ c = xs_regex_replace_i(c, " {2,}", " ");
+ c = xs_tolower_i(c);
+
+ while (!r && !feof(f)) {
+ xs *rx = xs_strip_i(xs_readline(f));
+
+ if (*rx) {
+ xs *l = xs_regex_select_n(c, rx, 1);
+
+ if (xs_list_len(l)) {
+ srv_debug(1, xs_fmt("content_check: match for '%s'", rx));
+ r = 1;
+ }
+ }
+ }
+
+ fclose(f);
+ }
+ }
+
+ return r;
+}
+
+
/** notifications **/
xs_str *notify_check_time(snac *snac, int reset)
@@ -2388,6 +2475,21 @@ void enqueue_verify_links(snac *user)
}
+void enqueue_actor_refresh(snac *user, const char *actor)
+/* enqueues an actor refresh */
+{
+ xs *qmsg = _new_qmsg("actor_refresh", "", 0);
+ char *ntid = xs_dict_get(qmsg, "ntid");
+ xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
+
+ qmsg = xs_dict_append(qmsg, "actor", actor);
+
+ qmsg = _enqueue_put(fn, qmsg);
+
+ snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
+}
+
+
void enqueue_request_replies(snac *user, const char *id)
/* enqueues a request for the replies of a message */
{
@@ -2645,6 +2747,16 @@ void purge_server(void)
}
}
}
+
+ /* delete index backups */
+ xs *specb = xs_fmt("%s/" "*.bak", v);
+ xs *bakfs = xs_glob(specb, 0, 0);
+
+ p2 = bakfs;
+ while (xs_list_iter(&p2, &v2)) {
+ unlink(v2);
+ srv_debug(1, xs_fmt("purged %s", v2));
+ }
}
}
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.
.It Cm request Ar basedir Ar uid Ar url
Requests an object and dumps it to stdout. This is a very low
level command that is not very useful to you.
-.It Cm note Ar basedir Ar uid Ar text
+.It Cm announce Ar basedir Ar uid Ar url
+Announces (boosts) a post via its URL.
+.It Cm note Ar basedir Ar uid Ar text Op file file ...
Enqueues a Create + Note message to all followers. If the
.Ar text
-argument is -e, the external editor defined by the EDITOR
+argument is -e, the external editor defined by the EDITOR
environment variable will be invoked to prepare a message; if
it's - (a lonely hyphen), the post content will be read from stdin.
+The rest of command line arguments are treated as media files to be
+attached to the post.
.It Cm block Ar basedir Ar instance_url
Blocks a full instance, given its URL or domain name. All subsequent
incoming 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
protocol and converted to links and mentions if something reasonable
is found.
.It Emoticons / Smileys / Silly Symbols
+(Note: from version 2.51, these symbols are configurable by the
+instance administrator, so the available ones may differ).
+.Pp
The following traditional ASCII emoticons or special strings are
converted to related emojis:
.Bd -literal
@@ -106,6 +109,13 @@ This file is served when the server base URL is requested from a web browser. Se
for more information about the customization options.
.It Pa public.idx
This file contains the list of public posts from all users in the server.
+.It Pa filter_reject.txt
+This (optional) file contains a list of regular expressions, one per line, to be
+applied to the content of all incoming posts; if any of them match, the post is
+rejected. This brings the flexibility and destruction power of regular expressions
+to your Fediverse experience. To be used wisely (see
+.Xr snac 8
+for more information).
.El
.Pp
Each 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
that will be served instead of the server-wide one.
It's not modifiable from the web interface to avoid users
shooting themselves in the foot by destroying everything.
-.Ss Old Data Purging
-From version 2.06, there is no longer a need to add a special
-cron job for purging old data, as this is managed internally.
+.Ss Custom Emojis
+From version 2.51, support for customized Emojis in posts is available
+(previously, they were hardcoded). Emojis are read from the
+.Pa emojis.json
+file in the instance base directory, as a JSON object of key / value
+pairs (if this file does not exist, it will be created with
+the predefined set). Each key in the object contains the text to be found (e.g.,
+the :-) for a smiling face), and its associated value, the text string that
+will replace it (in this example case, the HTML entity for the Unicode codepoint
+for the smiley or the Emoji itself as text).
+.Pp
+Emoji values can also be URLs to image files; in this case, they will not be
+substituted in the post content, but added to the 'tag' array as an ActivityPub
+standard 'Emoji' object (it's recommendable that the Emoji key be enclosed in
+colons for maximum compatilibity with other ActivityPub implementations, like
+e.g. :happydoggo:). These images can be served from an external source or from the
+.Pa static
+directory of the instance admin.
+.Pp
+If you want to disable any Emoji substitution, change the file to contain
+just an empty JSON object ({}).
+.Ss SPAM Mitigation
+There have been some SPAM attacks on the Fediverse and, as too many
+instances and server implementations out there still allow automatic
+account creation, it will only get worse.
+.Nm
+includes some (not very strong) tools for trying to survive the SPAM
+flood that will eventually happen.
+.Pp
+The
+.Ic min_account_age
+field in the main configuration file allows setting a minimum age (in
+seconds) to consider too recently created accounts suspicious of being
+a potential source of SPAM. This is a naïve assumption, because spammers
+can create accounts, let them dormant for a while and then start to use
+them. Also, some ActivityPub implementations don't even bother to return
+a creation date for their accounts, so this is not very useful.
+.Pp
+From version 2.50, post content can be filtered out by regular expressions.
+These weapons of mass destruction can be written into the
+.Ic filter_reject.txt
+file in the server base directory, one per line; if this file exists,
+all posts' content will be matched (after being stripped of HTML tags)
+against these regexes, one by one, and any match will make the post to
+be rejected. If you don't know about regular expressions, don't use this
+option (or learn about them in some tutorial, there are gazillions of
+them out there), as you and your users may start missing posts. Also,
+given that every regular expression implementation supports a different
+set of features, consider reading the documentation about the one
+implemented in your system.
.Ss ActivityPub Support
These are the following activities and objects that
.Nm
@@ -421,7 +468,7 @@ This is an example of a similar configuration for the Apache2 web server:
ProxyPreserveHost On
# Main web access point
-<Location /social>
+<Location /fedi>
ProxyPass http://127.0.0.1:8001/social
</Location>
@@ -481,7 +528,7 @@ an example:
# other server configuration
[...]
-location "/fedi*" {
+location "/fedi/*" {
fastcgi socket tcp "127.0.0.1" 8001
}
diff --git a/format.c b/format.c
index 9944822..92901bb 100644
--- a/format.c
+++ b/format.c
@@ -5,6 +5,8 @@
#include "xs_regex.h"
#include "xs_mime.h"
#include "xs_html.h"
+#include "xs_json.h"
+#include "xs_time.h"
#include "snac.h"
@@ -36,6 +38,46 @@ const char *smileys[] = {
};
+xs_dict *emojis(void)
+/* returns a dict with the emojis */
+{
+ xs *fn = xs_fmt("%s/emojis.json", srv_basedir);
+ FILE *f;
+
+ if (mtime(fn) == 0) {
+ /* file does not exist; create it with the defaults */
+ xs *d = xs_dict_new();
+ const char **emo = smileys;
+
+ while (*emo) {
+ d = xs_dict_append(d, emo[0], emo[1]);
+ emo += 2;
+ }
+
+ if ((f = fopen(fn, "w")) != NULL) {
+ xs_json_dump(d, 4, f);
+ fclose(f);
+ }
+ else
+ srv_log(xs_fmt("Error creating '%s'", fn));
+ }
+
+ xs_dict *d = NULL;
+
+ if ((f = fopen(fn, "r")) != NULL) {
+ d = xs_json_load(f);
+ fclose(f);
+
+ if (d == NULL)
+ srv_log(xs_fmt("JSON parse error in '%s'", fn));
+ }
+ else
+ srv_log(xs_fmt("Error opening '%s'", fn));
+
+ return d;
+}
+
+
static xs_str *format_line(const char *line, xs_list **attach)
/* formats a line */
{
@@ -106,7 +148,7 @@ static xs_str *format_line(const char *line, xs_list **attach)
}
-xs_str *not_really_markdown(const char *content, xs_list **attach)
+xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag)
/* formats a content using some Markdown rules */
{
xs_str *s = xs_str_new(NULL);
@@ -190,11 +232,36 @@ xs_str *not_really_markdown(const char *content, xs_list **attach)
{
/* traditional emoticons */
- const char **emo = smileys;
-
- while (*emo) {
- s = xs_replace_i(s, emo[0], emo[1]);
- emo += 2;
+ xs *d = emojis();
+ int c = 0;
+ char *k, *v;
+
+ while (xs_dict_next(d, &k, &v, &c)) {
+ const char *t = NULL;
+
+ /* is it an URL to an image? */
+ if (xs_startswith(v, "https:/" "/") && xs_startswith((t = xs_mime_by_ext(v)), "image/")) {
+ if (tag) {
+ /* add the emoji to the tag list */
+ xs *e = xs_dict_new();
+ xs *i = xs_dict_new();
+ xs *u = xs_str_utctime(0, ISO_DATE_SPEC);
+
+ e = xs_dict_append(e, "id", v);
+ e = xs_dict_append(e, "type", "Emoji");
+ e = xs_dict_append(e, "name", k);
+ e = xs_dict_append(e, "updated", u);
+
+ i = xs_dict_append(i, "type", "Image");
+ i = xs_dict_append(i, "mediaType", t);
+ i = xs_dict_append(i, "url", v);
+ e = xs_dict_append(e, "icon", i);
+
+ *tag = xs_list_append(*tag, e);
+ }
+ }
+ else
+ s = xs_replace_i(s, k, v);
}
}
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,
xs *name = actor_name(actor);
/* get the avatar */
- if ((v = xs_dict_get(actor, "icon")) != NULL &&
- (v = xs_dict_get(v, "url")) != NULL) {
- avatar = xs_dup(v);
+ if ((v = xs_dict_get(actor, "icon")) != NULL) {
+ /* if it's a list (Peertube), get the first one */
+ if (xs_type(v) == XSTYPE_LIST)
+ v = xs_list_get(v, 0);
+
+ if ((v = xs_dict_get(v, "url")) != NULL)
+ avatar = xs_dup(v);
}
if (avatar == NULL)
@@ -245,7 +249,7 @@ xs_html *html_msg_icon(snac *user, char *actor_id, const xs_dict *msg)
xs *actor = NULL;
xs_html *actor_icon = NULL;
- if (actor_id && valid_status(actor_get(actor_id, &actor))) {
+ if (actor_id && valid_status(actor_get_refresh(user, actor_id, &actor))) {
char *date = NULL;
char *udate = NULL;
char *url = NULL;
@@ -273,7 +277,8 @@ xs_html *html_note(snac *user, char *summary,
char *edit_id, char *actor_id,
xs_val *cw_yn, char *cw_text,
xs_val *mnt_only, char *redir,
- char *in_reply_to, int poll)
+ char *in_reply_to, int poll,
+ char *att_file, char *att_alt_text)
{
xs *action = xs_fmt("%s/admin/note", user->actor);
@@ -359,19 +364,37 @@ xs_html *html_note(snac *user, char *summary,
xs_html_attr("name", "edit_id"),
xs_html_attr("value", edit_id)));
+ /* attachment controls */
+ xs_html *att;
+
xs_html_add(form,
xs_html_tag("p", NULL),
- xs_html_tag("details",
+ att = xs_html_tag("details",
xs_html_tag("summary",
xs_html_text(L("Attachment..."))),
- xs_html_tag("p", NULL),
- xs_html_sctag("input",
- xs_html_attr("type", "file"),
- xs_html_attr("name", "attach")),
- xs_html_sctag("input",
- xs_html_attr("type", "text"),
- xs_html_attr("name", "alt_text"),
- xs_html_attr("placeholder", L("Attachment description")))));
+ xs_html_tag("p", NULL)));
+
+ if (att_file && *att_file)
+ xs_html_add(att,
+ xs_html_text(L("File:")),
+ xs_html_sctag("input",
+ xs_html_attr("type", "text"),
+ xs_html_attr("name", "attach_url"),
+ xs_html_attr("title", L("Clear this field to delete the attachment")),
+ xs_html_attr("value", att_file)));
+ else
+ xs_html_add(att,
+ xs_html_sctag("input",
+ xs_html_attr("type", "file"),
+ xs_html_attr("name", "attach")));
+
+ xs_html_add(att,
+ xs_html_text(" "),
+ xs_html_sctag("input",
+ xs_html_attr("type", "text"),
+ xs_html_attr("name", "alt_text"),
+ xs_html_attr("value", att_alt_text),
+ xs_html_attr("placeholder", L("Attachment description"))));
/* add poll controls */
if (poll) {
@@ -551,7 +574,7 @@ static xs_html *html_instance_body(char *tag)
}
-xs_html *html_user_head(snac *user, char *desc)
+xs_html *html_user_head(snac *user, char *desc, char *url)
{
xs_html *head = html_base_head();
@@ -641,6 +664,13 @@ xs_html *html_user_head(snac *user, char *desc)
xs_html_attr("title", "RSS"),
xs_html_attr("href", rss_url)));
+ /* ActivityPub alternate link (actor id) */
+ xs_html_add(head,
+ xs_html_sctag("link",
+ xs_html_attr("rel", "alternate"),
+ xs_html_attr("type", "application/activity+json"),
+ xs_html_attr("href", url ? url : user->actor)));
+
return head;
}
@@ -756,7 +786,7 @@ static xs_html *html_user_body(snac *user, int read_only)
if (read_only) {
xs *es1 = encode_html(xs_dict_get(user->config, "bio"));
- xs *bio1 = not_really_markdown(es1, NULL);
+ xs *bio1 = not_really_markdown(es1, NULL, NULL);
xs *tags = xs_list_new();
xs *bio2 = process_tags(user, bio1, &tags);
@@ -774,7 +804,7 @@ static xs_html *html_user_body(snac *user, int read_only)
xs_dict *val_links = user->links;
if (xs_is_null(val_links))
- val_links = xs_stock_dict;
+ val_links = xs_stock(XSTYPE_DICT);
xs_html *snac_metadata = xs_html_tag("div",
xs_html_attr("class", "snac-metadata"));
@@ -852,9 +882,9 @@ xs_html *html_top_controls(snac *snac)
"new_post_div", "new_post_form",
L("What's on your mind?"), "",
NULL, NULL,
- xs_stock_false, "",
- xs_stock_false, NULL,
- NULL, 1),
+ xs_stock(XSTYPE_FALSE), "",
+ xs_stock(XSTYPE_FALSE), NULL,
+ NULL, 1, "", ""),
/** operations **/
xs_html_tag("details",
@@ -1219,6 +1249,11 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
xs_html_add(form,
html_button("like", L("Like"), L("Say you like this post")));
}
+ else {
+ /* not like it anymore */
+ xs_html_add(form,
+ html_button("unlike", L("Unlike"), L("Nah don't like it that much")));
+ }
}
else {
if (is_pinned(snac, id))
@@ -1235,6 +1270,11 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
xs_html_add(form,
html_button("boost", L("Boost"), L("Announce this post to your followers")));
}
+ else {
+ /* already boosted; add button to regret */
+ xs_html_add(form,
+ html_button("unboost", L("Unboost"), L("I regret I boosted this")));
+ }
}
if (strcmp(actor, snac->actor) != 0) {
@@ -1278,6 +1318,20 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
xs *form_id = xs_fmt("%s_edit_form", md5);
xs *redir = xs_fmt("%s_entry", md5);
+ char *att_file = "";
+ char *att_alt_text = "";
+ xs_list *att_list = xs_dict_get(msg, "attachment");
+
+ /* does it have an attachment? */
+ if (xs_type(att_list) == XSTYPE_LIST && xs_list_len(att_list)) {
+ xs_dict *d = xs_list_get(att_list, 0);
+
+ if (xs_type(d) == XSTYPE_DICT) {
+ att_file = xs_dict_get_def(d, "url", "");
+ att_alt_text = xs_dict_get_def(d, "name", "");
+ }
+ }
+
xs_html_add(controls, xs_html_tag("div",
xs_html_tag("p", NULL),
html_note(snac, L("Edit..."),
@@ -1285,8 +1339,8 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
"", prev_src,
id, NULL,
xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
- xs_stock_false, redir,
- NULL, 0)),
+ xs_stock(XSTYPE_FALSE), redir,
+ NULL, 0, att_file, att_alt_text)),
xs_html_tag("p", NULL));
}
@@ -1304,8 +1358,8 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
"", ct,
NULL, NULL,
xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
- xs_stock_false, redir,
- id, 0)),
+ xs_stock(XSTYPE_FALSE), redir,
+ id, 0, "", "")),
xs_html_tag("p", NULL));
}
@@ -1858,6 +1912,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
xs_list *p = children;
char *cmd5;
+ int cnt = 0;
+ int o_cnt = 0;
+
while (xs_list_iter(&p, &cmd5)) {
xs *chd = NULL;
@@ -1866,23 +1923,40 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
else
object_get_by_md5(cmd5, &chd);
- if (chd != NULL && xs_is_null(xs_dict_get(chd, "name"))) {
- xs_html *che = html_entry(user, chd, read_only, level + 1, cmd5, hide_children);
+ if (chd != NULL) {
+ if (xs_is_null(xs_dict_get(chd, "name"))) {
+ xs_html *che = html_entry(user, chd, read_only,
+ level + 1, cmd5, hide_children);
+
+ if (che != NULL) {
+ if (left > 3) {
+ xs_html_add(ch_older,
+ che);
+
+ o_cnt++;
+ }
+ else
+ xs_html_add(ch_container,
+ che);
- if (che != NULL) {
- if (left > 3)
- xs_html_add(ch_older,
- che);
- else
- xs_html_add(ch_container,
- che);
+ cnt++;
+ }
}
+
+ left--;
}
else
srv_debug(2, xs_fmt("cannot read child %s", cmd5));
-
- left--;
}
+
+ /* if no children were finally added, hide the details */
+ if (cnt == 0)
+ xs_html_add(ch_details,
+ xs_html_attr("style", "display: none"));
+
+ if (o_cnt == 0 && ch_older)
+ xs_html_add(ch_older,
+ xs_html_attr("style", "display: none"));
}
}
@@ -1917,6 +1991,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
double t = ftime();
xs *desc = NULL;
+ xs *alternate = NULL;
if (xs_list_len(list) == 1) {
/* 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,
object_get_by_md5(id, &d);
if (d && (v = xs_dict_get(d, "sourceContent")) != NULL)
desc = xs_dup(v);
+
+ alternate = xs_dup(xs_dict_get(d, "id"));
}
xs_html *head;
xs_html *body;
if (user) {
- head = html_user_head(user, desc);
+ head = html_user_head(user, desc, alternate);
body = html_user_body(user, read_only);
}
else {
@@ -1977,9 +2054,16 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
if (user != NULL && !is_msg_public(msg)) {
char *irt = xs_dict_get(msg, "inReplyTo");
+ /* is it a reply to something not in the storage? */
if (!xs_is_null(irt) && !object_here(irt)) {
- snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
- continue;
+ /* is it for me? */
+ xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
+ xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
+
+ if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
+ snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
+ continue;
+ }
}
}
@@ -2077,9 +2161,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t)
snac_posts = xs_html_tag("details",
xs_html_attr("open", NULL),
xs_html_tag("summary",
- xs_html_text("...")),
- xs_html_tag("div",
- xs_html_attr("class", "snac-posts"))));
+ xs_html_text("..."))));
xs_list *p = list;
char *actor_id;
@@ -2181,9 +2263,9 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t)
dm_div_id, dm_form_id,
"", "",
NULL, actor_id,
- xs_stock_false, "",
- xs_stock_false, NULL,
- NULL, 0),
+ xs_stock(XSTYPE_FALSE), "",
+ xs_stock(XSTYPE_FALSE), NULL,
+ NULL, 0, "", ""),
xs_html_tag("p", NULL));
xs_html_add(snac_post, snac_controls);
@@ -2202,10 +2284,12 @@ xs_str *html_people(snac *user)
xs *wers = follower_list(user);
xs_html *html = xs_html_tag("html",
- html_user_head(user, NULL),
+ html_user_head(user, NULL, NULL),
xs_html_add(html_user_body(user, 0),
- html_people_list(user, wing, L("People you follow"), "i"),
- html_people_list(user, wers, L("People that follow you"), "e"),
+ xs_html_tag("div",
+ xs_html_attr("class", "snac-posts"),
+ html_people_list(user, wing, L("People you follow"), "i"),
+ html_people_list(user, wers, L("People that follow you"), "e")),
html_footer()));
return xs_html_render_s(html, "<!DOCTYPE html>\n");
@@ -2220,7 +2304,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
xs_html *body = html_user_body(user, 0);
xs_html *html = xs_html_tag("html",
- html_user_head(user, NULL),
+ html_user_head(user, NULL, NULL),
body);
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,
return 403;
xs *elems = timeline_simple_list(&snac, "public", 0, 20);
- xs *bio = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL);
+ xs *bio = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL, NULL);
xs *rss_title = xs_fmt("%s (@%s@%s)",
xs_dict_get(snac.config, "name"),
@@ -2802,7 +2886,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv);
if (sensitive != NULL) {
- msg = xs_dict_set(msg, "sensitive", xs_stock_true);
+ msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary);
}
@@ -2881,6 +2965,22 @@ int html_post_handler(const xs_dict *req, const char *q_path,
}
}
else
+ if (strcmp(action, L("Unlike")) == 0) { /** **/
+ xs *msg = msg_repulsion(&snac, id, "Like");
+
+ if (msg != NULL) {
+ enqueue_message(&snac, msg);
+ }
+ }
+ else
+ if (strcmp(action, L("Unboost")) == 0) { /** **/
+ xs *msg = msg_repulsion(&snac, id, "Announce");
+
+ if (msg != NULL) {
+ enqueue_message(&snac, msg);
+ }
+ }
+ else
if (strcmp(action, L("MUTE")) == 0) { /** **/
mute(&snac, actor);
}
@@ -3036,17 +3136,17 @@ int html_post_handler(const xs_dict *req, const char *q_path,
snac.config = xs_dict_set(snac.config, "purge_days", days);
}
if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
- snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock_true);
+ snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE));
else
- snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock_false);
+ snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE));
if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
- snac.config = xs_dict_set(snac.config, "bot", xs_stock_true);
+ snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE));
else
- snac.config = xs_dict_set(snac.config, "bot", xs_stock_false);
+ snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE));
if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
- snac.config = xs_dict_set(snac.config, "private", xs_stock_true);
+ snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE));
else
- snac.config = xs_dict_set(snac.config, "private", xs_stock_false);
+ snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE));
if ((v = xs_dict_get(p_vars, "metadata")) != NULL) {
/* split the metadata and store it as a dict */
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)
body, xs_dict_get(srv_config, "host"));
headers = xs_dict_append(headers, "WWW-Authenticate", www_auth);
+ headers = xs_dict_append(headers, "Cache-Control", "no-cache, must-revalidate, max-age=0");
}
if (ctype == NULL)
@@ -814,7 +815,7 @@ void httpd(void)
/* send as many exit jobs as working threads */
for (n = 1; n < p_state->n_threads; n++)
- job_post(xs_stock_false, 0);
+ job_post(xs_stock(XSTYPE_FALSE), 0);
/* wait for all the threads to exit */
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 @@
#include "xs_io.h"
#include "xs_json.h"
#include "xs_time.h"
+#include "xs_openssl.h"
#include "snac.h"
@@ -17,30 +18,32 @@ int usage(void)
printf("\n");
printf("Commands:\n");
printf("\n");
- printf("init [{basedir}] Initializes the data storage\n");
- printf("upgrade {basedir} Upgrade to a new version\n");
- printf("adduser {basedir} [{uid}] Adds a new user\n");
- printf("deluser {basedir} {uid} Deletes a user\n");
- printf("httpd {basedir} Starts the HTTPD daemon\n");
- printf("purge {basedir} Purges old data\n");
- printf("state {basedir} Prints server state\n");
- printf("webfinger {basedir} {actor} Queries about an actor (@user@host or actor url)\n");
- printf("queue {basedir} {uid} Processes a user queue\n");
- printf("follow {basedir} {uid} {actor} Follows an actor\n");
- printf("unfollow {basedir} {uid} {actor} Unfollows an actor\n");
- printf("request {basedir} {uid} {url} Requests an object\n");
- printf("actor {basedir} [{uid}] {url} Requests an actor\n");
- printf("note {basedir} {uid} {'text'} Sends a note to followers\n");
- printf("resetpwd {basedir} {uid} Resets the password of a user\n");
- printf("ping {basedir} {uid} {actor} Pings an actor\n");
- printf("webfinger_s {basedir} {uid} {actor} Queries about an actor (@user@host or actor url)\n");
- printf("pin {basedir} {uid} {msg_url} Pins a message\n");
- printf("unpin {basedir} {uid} {msg_url} Unpins a message\n");
- printf("block {basedir} {instance_url} Blocks a full instance\n");
- printf("unblock {basedir} {instance_url} Unblocks a full instance\n");
- printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n");
- printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n");
- printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n");
+ printf("init [{basedir}] Initializes the data storage\n");
+ printf("upgrade {basedir} Upgrade to a new version\n");
+ printf("adduser {basedir} [{uid}] Adds a new user\n");
+ printf("deluser {basedir} {uid} Deletes a user\n");
+ printf("httpd {basedir} Starts the HTTPD daemon\n");
+ printf("purge {basedir} Purges old data\n");
+ printf("state {basedir} Prints server state\n");
+ printf("webfinger {basedir} {actor} Queries about an actor (@user@host or actor url)\n");
+ printf("queue {basedir} {uid} Processes a user queue\n");
+ printf("follow {basedir} {uid} {actor} Follows an actor\n");
+ printf("unfollow {basedir} {uid} {actor} Unfollows an actor\n");
+ printf("request {basedir} {uid} {url} Requests an object\n");
+ printf("actor {basedir} [{uid}] {url} Requests an actor\n");
+ printf("note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n");
+ printf("boost|announce {basedir} {uid} {url} Boosts (announces) a post\n");
+ printf("unboost {basedir} {uid} {url} Unboosts a post\n");
+ printf("resetpwd {basedir} {uid} Resets the password of a user\n");
+ printf("ping {basedir} {uid} {actor} Pings an actor\n");
+ printf("webfinger_s {basedir} {uid} {actor} Queries about an actor (@user@host or actor url)\n");
+ printf("pin {basedir} {uid} {msg_url} Pins a message\n");
+ printf("unpin {basedir} {uid} {msg_url} Unpins a message\n");
+ printf("block {basedir} {instance_url} Blocks a full instance\n");
+ printf("unblock {basedir} {instance_url} Unblocks a full instance\n");
+ printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n");
+ printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n");
+ printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n");
return 1;
}
@@ -96,7 +99,7 @@ int main(int argc, char *argv[])
if (strcmp(cmd, "markdown") == 0) { /** **/
/* undocumented, for testing only */
xs *c = xs_readall(stdin);
- xs *fc = not_really_markdown(c, NULL);
+ xs *fc = not_really_markdown(c, NULL, NULL);
printf("<html>\n%s\n</html>\n", fc);
return 0;
@@ -279,7 +282,7 @@ int main(int argc, char *argv[])
return 0;
}
- if (strcmp(cmd, "announce") == 0) { /** **/
+ if (strcmp(cmd, "boost") == 0 || strcmp(cmd, "announce") == 0) { /** **/
xs *msg = msg_admiration(&snac, url, "Announce");
if (msg != NULL) {
@@ -293,6 +296,20 @@ int main(int argc, char *argv[])
return 0;
}
+ if (strcmp(cmd, "unboost") == 0) { /** **/
+ xs *msg = msg_repulsion(&snac, url, "Announce");
+
+ if (msg != NULL) {
+ enqueue_message(&snac, msg);
+
+ if (dbglevel) {
+ xs_json_dump(msg, 4, stdout);
+ }
+ }
+
+ return 0;
+ }
+
if (strcmp(cmd, "follow") == 0) { /** **/
xs *msg = msg_follow(&snac, url);
@@ -360,6 +377,14 @@ int main(int argc, char *argv[])
if (strcmp(cmd, "ping") == 0) { /** **/
xs *actor_o = NULL;
+ if (!xs_startswith(url, "https:/")) {
+ /* try to resolve via webfinger */
+ if (!valid_status(webfinger_request(url, &url, NULL))) {
+ srv_log(xs_fmt("cannot resolve %s via webfinger", url));
+ return 1;
+ }
+ }
+
if (valid_status(actor_request(&snac, url, &actor_o))) {
xs *msg = msg_ping(&snac, url);
@@ -368,6 +393,8 @@ int main(int argc, char *argv[])
if (dbglevel) {
xs_json_dump(msg, 4, stdout);
}
+
+ srv_log(xs_fmt("Ping sent to %s -- see log for Pong reply", url));
}
else {
srv_log(xs_fmt("Error getting actor %s", url));
@@ -450,7 +477,39 @@ int main(int argc, char *argv[])
xs *content = NULL;
xs *msg = NULL;
xs *c_msg = NULL;
- char *in_reply_to = GET_ARGV();
+ xs *attl = xs_list_new();
+ char *fn = NULL;
+
+ /* iterate possible attachments */
+ while ((fn = GET_ARGV())) {
+ FILE *f;
+
+ if ((f = fopen(fn, "rb")) != NULL) {
+ /* get the file size and content */
+ fseek(f, 0, SEEK_END);
+ int sz = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ xs *atc = xs_readall(f);
+ fclose(f);
+
+ char *ext = strrchr(fn, '.');
+ xs *hash = xs_md5_hex(fn, strlen(fn));
+ xs *id = xs_fmt("%s%s", hash, ext);
+ xs *url = xs_fmt("%s/s/%s", snac.actor, id);
+
+ /* store */
+ static_put(&snac, id, atc, sz);
+
+ xs *l = xs_list_new();
+
+ l = xs_list_append(l, url);
+ l = xs_list_append(l, ""); /* alt text */
+
+ attl = xs_list_append(attl, l);
+ }
+ else
+ fprintf(stderr, "Error opening '%s' as attachment\n", fn);
+ }
if (strcmp(url, "-e") == 0) {
/* get the content from an editor */
@@ -478,7 +537,7 @@ int main(int argc, char *argv[])
else
content = xs_dup(url);
- msg = msg_note(&snac, content, NULL, in_reply_to, NULL, 0);
+ msg = msg_note(&snac, content, NULL, NULL, attl, 0);
c_msg = msg_create(&snac, msg);
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,
*body = xs_dup(code);
}
else {
- *body = xs_fmt("%s?code=%s", redir, code);
+ if (xs_str_in(redir, "?"))
+ *body = xs_fmt("%s&code=%s", redir, code);
+ else
+ *body = xs_fmt("%s?code=%s", redir, code);
+
status = 303;
}
@@ -335,8 +339,8 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
/* FIXME: this 'scope' parameter is mandatory for the official Mastodon API,
but if it's enabled, it makes it crash after some more steps, which
is FAR WORSE */
-// const char *scope = xs_dict_get(args, "scope");
const char *scope = NULL;
+// scope = xs_dict_get(args, "scope");
/* no client_secret? check if it's inside an authorization header
(AndStatus does it this way) */
@@ -359,9 +363,9 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
}
}
- /* no code?
- I'm not sure of the impacts of this right now, but Subway Tooter does not
- provide a code so one must be generated */
+ /* no code?
+ I'm not sure of the impacts of this right now, but Subway Tooter does not
+ provide a code so one must be generated */
if (xs_is_null(code)){
code = random_str();
}
@@ -522,6 +526,12 @@ xs_dict *mastoapi_account(const xs_dict *actor)
acct = xs_dict_append(acct, "id", acct_md5);
acct = xs_dict_append(acct, "username", prefu);
acct = xs_dict_append(acct, "display_name", display_name);
+ acct = xs_dict_append(acct, "discoverable", xs_stock(XSTYPE_TRUE));
+ acct = xs_dict_append(acct, "group", xs_stock(XSTYPE_FALSE));
+ acct = xs_dict_append(acct, "hide_collections", xs_stock(XSTYPE_FALSE));
+ acct = xs_dict_append(acct, "indexable", xs_stock(XSTYPE_TRUE));
+ acct = xs_dict_append(acct, "noindex", xs_stock(XSTYPE_FALSE));
+ acct = xs_dict_append(acct, "roles", xs_stock(XSTYPE_LIST));
{
/* create the acct field as user@host */
@@ -543,13 +553,14 @@ xs_dict *mastoapi_account(const xs_dict *actor)
note = "";
if (strcmp(xs_dict_get(actor, "type"), "Service") == 0)
- acct = xs_dict_append(acct, "bot", xs_stock_true);
+ acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_TRUE));
else
- acct = xs_dict_append(acct, "bot", xs_stock_false);
+ acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_FALSE));
acct = xs_dict_append(acct, "note", note);
acct = xs_dict_append(acct, "url", id);
+ acct = xs_dict_append(acct, "uri", id);
xs *avatar = NULL;
xs_dict *av = xs_dict_get(actor, "icon");
@@ -574,7 +585,7 @@ xs_dict *mastoapi_account(const xs_dict *actor)
header = xs_dup(xs_dict_get(hd, "url"));
if (xs_is_null(header))
- header = xs_dup("");
+ header = xs_fmt("%s/header.png", srv_baseurl);
acct = xs_dict_append(acct, "header", header);
acct = xs_dict_append(acct, "header_static", header);
@@ -602,7 +613,7 @@ xs_dict *mastoapi_account(const xs_dict *actor)
d1 = xs_dict_append(d1, "shortcode", nm);
d1 = xs_dict_append(d1, "url", url);
d1 = xs_dict_append(d1, "static_url", url);
- d1 = xs_dict_append(d1, "visible_in_picker", xs_stock_true);
+ d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
eml = xs_list_append(eml, d1);
}
@@ -613,10 +624,10 @@ xs_dict *mastoapi_account(const xs_dict *actor)
acct = xs_dict_append(acct, "emojis", eml);
}
- acct = xs_dict_append(acct, "locked", xs_stock_false);
- acct = xs_dict_append(acct, "followers_count", xs_stock_0);
- acct = xs_dict_append(acct, "following_count", xs_stock_0);
- acct = xs_dict_append(acct, "statuses_count", xs_stock_0);
+ acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
+ acct = xs_dict_append(acct, "followers_count", xs_stock(0));
+ acct = xs_dict_append(acct, "following_count", xs_stock(0));
+ acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
xs *fields = xs_list_new();
p = xs_dict_get(actor, "attachment");
@@ -624,19 +635,19 @@ xs_dict *mastoapi_account(const xs_dict *actor)
/* dict of validated links */
xs_dict *val_links = NULL;
- xs_dict *metadata = xs_stock_dict;
+ xs_dict *metadata = xs_stock(XSTYPE_DICT);
snac user = {0};
if (xs_startswith(id, srv_baseurl)) {
/* if it's a local user, open it and pick its validated links */
if (user_open(&user, prefu)) {
val_links = user.links;
- metadata = xs_dict_get_def(user.config, "metadata", xs_stock_dict);
+ metadata = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT));
}
}
if (xs_is_null(val_links))
- val_links = xs_stock_dict;
+ val_links = xs_stock(XSTYPE_DICT);
while (xs_list_iter(&p, &v)) {
char *type = xs_dict_get(v, "type");
@@ -665,7 +676,7 @@ xs_dict *mastoapi_account(const xs_dict *actor)
d = xs_dict_append(d, "value", value);
d = xs_dict_append(d, "verified_at",
xs_type(val_date) == XSTYPE_STRING && *val_date ?
- val_date : xs_stock_null);
+ val_date : xs_stock(XSTYPE_NULL));
fields = xs_list_append(fields, d);
}
@@ -703,13 +714,13 @@ xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg)
xs *fd = mastoapi_date(xs_dict_get(msg, "endTime"));
poll = xs_dict_append(poll, "expires_at", fd);
poll = xs_dict_append(poll, "expired",
- xs_dict_get(msg, "closed") != NULL ? xs_stock_true : xs_stock_false);
+ xs_dict_get(msg, "closed") != NULL ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
if ((opts = xs_dict_get(msg, "oneOf")) != NULL)
- poll = xs_dict_append(poll, "multiple", xs_stock_false);
+ poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_FALSE));
else {
opts = xs_dict_get(msg, "anyOf");
- poll = xs_dict_append(poll, "multiple", xs_stock_true);
+ poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_TRUE));
}
while (xs_list_iter(&opts, &v)) {
@@ -736,7 +747,7 @@ xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg)
poll = xs_dict_append(poll, "voted",
(snac && was_question_voted(snac, xs_dict_get(msg, "id"))) ?
- xs_stock_true : xs_stock_false);
+ xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
return poll;
}
@@ -746,7 +757,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
/* converts an ActivityPub note to a Mastodon status */
{
xs *actor = NULL;
- actor_get(get_atto(msg), &actor);
+ actor_get_refresh(snac, get_atto(msg), &actor);
/* if the author is not here, discard */
if (actor == NULL)
@@ -802,7 +813,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
tmp = xs_dict_get(msg, "sensitive");
if (xs_is_null(tmp))
- tmp = xs_stock_false;
+ tmp = xs_stock(XSTYPE_FALSE);
st = xs_dict_append(st, "sensitive", tmp);
@@ -921,7 +932,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
d1 = xs_dict_append(d1, "shortcode", nm);
d1 = xs_dict_append(d1, "url", url);
d1 = xs_dict_append(d1, "static_url", url);
- d1 = xs_dict_append(d1, "visible_in_picker", xs_stock_true);
+ d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
d1 = xs_dict_append(d1, "category", "Emojis");
eml = xs_list_append(eml, d1);
@@ -942,7 +953,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
st = xs_dict_append(st, "favourites_count", ixc);
st = xs_dict_append(st, "favourited",
- (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock_true : xs_stock_false);
+ (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
xs_free(idx);
xs_free(ixc);
@@ -951,7 +962,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
st = xs_dict_append(st, "reblogs_count", ixc);
st = xs_dict_append(st, "reblogged",
- (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock_true : xs_stock_false);
+ (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
/* get the last person who boosted this */
xs *boosted_by_md5 = NULL;
@@ -966,8 +977,8 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
st = xs_dict_append(st, "replies_count", ixc);
/* default in_reply_to values */
- st = xs_dict_append(st, "in_reply_to_id", xs_stock_null);
- st = xs_dict_append(st, "in_reply_to_account_id", xs_stock_null);
+ st = xs_dict_append(st, "in_reply_to_id", xs_stock(XSTYPE_NULL));
+ st = xs_dict_append(st, "in_reply_to_account_id", xs_stock(XSTYPE_NULL));
tmp = xs_dict_get(msg, "inReplyTo");
if (!xs_is_null(tmp)) {
@@ -985,9 +996,9 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
}
}
- st = xs_dict_append(st, "reblog", xs_stock_null);
- st = xs_dict_append(st, "card", xs_stock_null);
- st = xs_dict_append(st, "language", xs_stock_null);
+ st = xs_dict_append(st, "reblog", xs_stock(XSTYPE_NULL));
+ st = xs_dict_append(st, "card", xs_stock(XSTYPE_NULL));
+ st = xs_dict_append(st, "language", xs_stock(XSTYPE_NULL));
tmp = xs_dict_get(msg, "sourceContent");
if (xs_is_null(tmp))
@@ -998,7 +1009,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
tmp = xs_dict_get(msg, "updated");
xs *fd2 = NULL;
if (xs_is_null(tmp))
- tmp = xs_stock_null;
+ tmp = xs_stock(XSTYPE_NULL);
else {
fd2 = mastoapi_date(tmp);
tmp = fd2;
@@ -1011,12 +1022,12 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
st = xs_dict_append(st, "poll", poll);
}
else
- st = xs_dict_append(st, "poll", xs_stock_null);
+ st = xs_dict_append(st, "poll", xs_stock(XSTYPE_NULL));
- st = xs_dict_append(st, "bookmarked", xs_stock_false);
+ st = xs_dict_append(st, "bookmarked", xs_stock(XSTYPE_FALSE));
st = xs_dict_append(st, "pinned",
- (snac && is_pinned(snac, id)) ? xs_stock_true : xs_stock_false);
+ (snac && is_pinned(snac, id)) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
/* is it a boost? */
if (!xs_is_null(boosted_by_md5)) {
@@ -1060,21 +1071,21 @@ xs_dict *mastoapi_relationship(snac *snac, const char *md5)
rel = xs_dict_append(rel, "id", md5);
rel = xs_dict_append(rel, "following",
- following_check(snac, actor) ? xs_stock_true : xs_stock_false);
+ following_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
- rel = xs_dict_append(rel, "showing_reblogs", xs_stock_true);
- rel = xs_dict_append(rel, "notifying", xs_stock_false);
+ rel = xs_dict_append(rel, "showing_reblogs", xs_stock(XSTYPE_TRUE));
+ rel = xs_dict_append(rel, "notifying", xs_stock(XSTYPE_FALSE));
rel = xs_dict_append(rel, "followed_by",
- follower_check(snac, actor) ? xs_stock_true : xs_stock_false);
+ follower_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
rel = xs_dict_append(rel, "blocking",
- is_muted(snac, actor) ? xs_stock_true : xs_stock_false);
+ is_muted(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
- rel = xs_dict_append(rel, "muting", xs_stock_false);
- rel = xs_dict_append(rel, "muting_notifications", xs_stock_false);
- rel = xs_dict_append(rel, "requested", xs_stock_false);
- rel = xs_dict_append(rel, "domain_blocking", xs_stock_false);
- rel = xs_dict_append(rel, "endorsed", xs_stock_false);
+ rel = xs_dict_append(rel, "muting", xs_stock(XSTYPE_FALSE));
+ rel = xs_dict_append(rel, "muting_notifications", xs_stock(XSTYPE_FALSE));
+ rel = xs_dict_append(rel, "requested", xs_stock(XSTYPE_FALSE));
+ rel = xs_dict_append(rel, "domain_blocking", xs_stock(XSTYPE_FALSE));
+ rel = xs_dict_append(rel, "endorsed", xs_stock(XSTYPE_FALSE));
rel = xs_dict_append(rel, "note", "");
}
@@ -1142,9 +1153,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac1.config, "published"));
acct = xs_dict_append(acct, "note", xs_dict_get(snac1.config, "bio"));
acct = xs_dict_append(acct, "url", snac1.actor);
- acct = xs_dict_append(acct, "header", "");
- acct = xs_dict_append(acct, "header_static", "");
- acct = xs_dict_append(acct, "locked", xs_stock_false);
+ acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
acct = xs_dict_append(acct, "bot", xs_dict_get(snac1.config, "bot"));
xs *src = xs_json_loads("{\"privacy\":\"public\","
@@ -1162,6 +1171,17 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
acct = xs_dict_append(acct, "avatar", avatar);
acct = xs_dict_append(acct, "avatar_static", avatar);
+ xs *header = NULL;
+ char *hd = xs_dict_get(snac1.config, "header");
+
+ if (!xs_is_null(hd))
+ header = xs_dup(hd);
+ else
+ header = xs_fmt("%s/header.png", srv_baseurl);
+
+ acct = xs_dict_append(acct, "header", header);
+ acct = xs_dict_append(acct, "header_static", header);
+
xs_dict *metadata = xs_dict_get(snac1.config, "metadata");
if (xs_type(metadata) == XSTYPE_DICT) {
xs *fields = xs_list_new();
@@ -1170,7 +1190,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
xs_dict *val_links = snac1.links;
if (xs_is_null(val_links))
- val_links = xs_stock_dict;
+ val_links = xs_stock(XSTYPE_DICT);
int c = 0;
while (xs_dict_next(metadata, &k, &v, &c)) {
@@ -1190,7 +1210,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
d = xs_dict_append(d, "value", v);
d = xs_dict_append(d, "verified_at",
xs_type(val_date) == XSTYPE_STRING && *val_date ?
- val_date : xs_stock_null);
+ val_date : xs_stock(XSTYPE_NULL));
fields = xs_list_append(fields, d);
}
@@ -1198,9 +1218,9 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
acct = xs_dict_set(acct, "fields", fields);
}
- acct = xs_dict_append(acct, "followers_count", xs_stock_0);
- acct = xs_dict_append(acct, "following_count", xs_stock_0);
- acct = xs_dict_append(acct, "statuses_count", xs_stock_0);
+ acct = xs_dict_append(acct, "followers_count", xs_stock(0));
+ acct = xs_dict_append(acct, "following_count", xs_stock(0));
+ acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
*body = xs_json_dumps(acct, 4);
*ctype = "application/json";
@@ -1716,8 +1736,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
else
if (strcmp(cmd, "/v2/filters") == 0) { /** **/
/* snac will never have filters
- * but still, without a v2 endpoint a short delay is introduced
- * in some apps */
+ * but still, without a v2 endpoint a short delay is introduced
+ * in some apps */
*body = xs_dup("[]");
*ctype = "application/json";
status = 200;
@@ -1797,7 +1817,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
ins = xs_dict_append(ins, "email", v);
- ins = xs_dict_append(ins, "rules", xs_stock_list);
+ ins = xs_dict_append(ins, "rules", xs_stock(XSTYPE_LIST));
xs *l1 = xs_list_append(xs_list_new(), "en");
ins = xs_dict_append(ins, "languages", l1);
@@ -1808,14 +1828,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
ins = xs_dict_append(ins, "urls", urls);
- xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock_0);
- d2 = xs_dict_append(d2, "status_count", xs_stock_0);
- d2 = xs_dict_append(d2, "domain_count", xs_stock_0);
+ xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock(0));
+ d2 = xs_dict_append(d2, "status_count", xs_stock(0));
+ d2 = xs_dict_append(d2, "domain_count", xs_stock(0));
ins = xs_dict_append(ins, "stats", d2);
- ins = xs_dict_append(ins, "registrations", xs_stock_false);
- ins = xs_dict_append(ins, "approval_required", xs_stock_false);
- ins = xs_dict_append(ins, "invites_enabled", xs_stock_false);
+ ins = xs_dict_append(ins, "registrations", xs_stock(XSTYPE_FALSE));
+ ins = xs_dict_append(ins, "approval_required", xs_stock(XSTYPE_FALSE));
+ ins = xs_dict_append(ins, "invites_enabled", xs_stock(XSTYPE_FALSE));
xs *cfg = xs_dict_new();
@@ -2063,7 +2083,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
d = xs_dict_append(d, "name", q);
xs *url = xs_fmt("%s?t=%s", srv_baseurl, q);
d = xs_dict_append(d, "url", url);
- d = xs_dict_append(d, "history", xs_stock_list);
+ d = xs_dict_append(d, "history", xs_stock(XSTYPE_LIST));
htl = xs_list_append(htl, d);
}
@@ -2103,8 +2123,6 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
return 0;
- srv_debug(1, xs_fmt("mastoapi_post_handler %s", q_path));
-
int status = 404;
xs *args = NULL;
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,
}
else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
{
- // Some apps send form data instead of json so we should cater for those
+ // Some apps send form data instead of json so we should cater for those
if (!xs_is_null(payload)) {
xs *upl = xs_url_dec(payload);
args = xs_url_vars(upl);
@@ -2241,7 +2259,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
strcmp(visibility, "public") == 0 ? 0 : 1);
if (!xs_is_null(summary) && *summary) {
- msg = xs_dict_set(msg, "sensitive", xs_stock_true);
+ msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
msg = xs_dict_set(msg, "summary", summary);
}
@@ -2298,11 +2316,13 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
}
else
if (strcmp(op, "unfavourite") == 0) { /** **/
- /* partial support: as the original Like message
- is not stored anywhere here, it's not possible
- to send an Undo + Like; the only thing done here
- is to delete the actor from the list of likes */
- object_unadmire(id, snac.actor, 1);
+ xs *n_msg = msg_repulsion(&snac, id, "Like");
+
+ if (n_msg != NULL) {
+ enqueue_message(&snac, n_msg);
+
+ out = mastoapi_status(&snac, msg);
+ }
}
else
if (strcmp(op, "reblog") == 0) { /** **/
@@ -2317,8 +2337,13 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
}
else
if (strcmp(op, "unreblog") == 0) { /** **/
- /* partial support: see comment in 'unfavourite' */
- object_unadmire(id, snac.actor, 0);
+ xs *n_msg = msg_repulsion(&snac, id, "Announce");
+
+ if (n_msg != NULL) {
+ enqueue_message(&snac, n_msg);
+
+ out = mastoapi_status(&snac, msg);
+ }
}
else
if (strcmp(op, "bookmark") == 0) { /** **/
@@ -2603,6 +2628,8 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
if (logged_in)
user_free(&snac);
+ srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status));
+
return status;
}
@@ -2701,7 +2728,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) {
const char *content = xs_dict_get(args, "status");
xs *atls = xs_list_new();
- xs *f_content = not_really_markdown(content, &atls);
+ xs *f_content = not_really_markdown(content, &atls, NULL);
/* replace fields with new content */
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 @@
/* snac - A simple, minimalistic ActivityPub instance */
/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
-#define VERSION "2.50-dev"
+#define VERSION "2.52-dev"
#define USER_AGENT "snac/" VERSION
@@ -110,6 +110,10 @@ int object_del(const char *id);
int object_del_if_unref(const char *id);
double object_ctime_by_md5(const char *md5);
double object_ctime(const char *id);
+double object_mtime_by_md5(const char *md5);
+double object_mtime(const char *id);
+void object_touch(const char *id);
+
int object_admire(const char *id, const char *actor, int like);
int object_unadmire(const char *id, const char *actor, int like);
@@ -172,6 +176,7 @@ xs_list *tag_search(char *tag, int skip, int show);
int actor_add(const char *actor, xs_dict *msg);
int actor_get(const char *actor, xs_dict **data);
+int actor_get_refresh(snac *user, const char *actor, xs_dict **data);
int static_get(snac *snac, const char *id, xs_val **data, int *size, const char *inm, xs_str **etag);
void static_put(snac *snac, const char *id, const char *data, int size);
@@ -204,6 +209,8 @@ int is_instance_blocked(const char *instance);
int instance_block(const char *instance);
int instance_unblock(const char *instance);
+int content_check(const char *file, const xs_dict *msg);
+
void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries);
void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries);
void 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
void enqueue_message(snac *snac, const xs_dict *msg);
void enqueue_close_question(snac *user, const char *id, int end_secs);
void enqueue_verify_links(snac *user);
+void enqueue_actor_refresh(snac *user, const char *actor);
void enqueue_request_replies(snac *user, const char *id);
int was_question_voted(snac *user, const char *id);
@@ -256,6 +264,7 @@ char *get_atto(const xs_dict *msg);
xs_list *get_attachments(const xs_dict *msg);
xs_dict *msg_admiration(snac *snac, char *object, char *type);
+xs_dict *msg_repulsion(snac *user, char *id, char *type);
xs_dict *msg_create(snac *snac, const xs_dict *object);
xs_dict *msg_follow(snac *snac, const char *actor);
@@ -296,7 +305,8 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
char *payload, int p_size,
char **body, int *b_size, char **ctype);
-xs_str *not_really_markdown(const char *content, xs_list **attach);
+xs_dict *emojis(void);
+xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag);
xs_str *sanitize(const char *content);
xs_str *encode_html(const char *str);
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;
/* not really all, just very much */
#define XS_ALL 0xfffffff
+#ifndef xs_countof
+#define xs_countof(a) (sizeof((a)) / sizeof((*a)))
+#endif
+
void *xs_free(void *ptr);
void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func);
#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);
xs_val *xs_insert_m(xs_val *data, int offset, const char *mem, int size);
#define xs_insert(data, offset, data2) xs_insert_m(data, offset, data2, xs_size(data2))
#define xs_append_m(data, mem, size) xs_insert_m(data, xs_size(data) - 1, mem, size)
+xs_val *xs_stock(int type);
xs_str *xs_str_new(const char *str);
xs_str *xs_str_new_sz(const char *mem, int sz);
@@ -137,34 +142,11 @@ unsigned int xs_hash_func(const char *data, int size);
#define XS_ASSERT_TYPE_NULL(v, t) (void)(0)
#endif
-extern xs_val xs_stock_null[];
-extern xs_val xs_stock_true[];
-extern xs_val xs_stock_false[];
-extern xs_val xs_stock_0[];
-extern xs_val xs_stock_1[];
-extern xs_val xs_stock_list[];
-extern xs_val xs_stock_dict[];
-
#define xs_return(v) xs_val *__r = v; v = NULL; return __r
#ifdef XS_IMPLEMENTATION
-xs_val xs_stock_null[] = { XSTYPE_NULL };
-xs_val xs_stock_true[] = { XSTYPE_TRUE };
-xs_val xs_stock_false[] = { XSTYPE_FALSE };
-xs_val xs_stock_0[] = { XSTYPE_NUMBER, '0', '\0' };
-xs_val xs_stock_1[] = { XSTYPE_NUMBER, '1', '\0' };
-
-#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
-xs_val xs_stock_list[] = { XSTYPE_LIST, 0, 0, 0, 1 + _XS_TYPE_SIZE + 1, XSTYPE_EOM };
-xs_val xs_stock_dict[] = { XSTYPE_DICT, 0, 0, 0, 1 + _XS_TYPE_SIZE + 1, XSTYPE_EOM };
-#else
-xs_val xs_stock_list[] = { XSTYPE_LIST, 1 + _XS_TYPE_SIZE + 1, 0, 0, 0, XSTYPE_EOM };
-xs_val xs_stock_dict[] = { XSTYPE_DICT, 1 + _XS_TYPE_SIZE + 1, 0, 0, 0, XSTYPE_EOM };
-#endif
-
-
void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func)
{
xs_val *ndata = realloc(ptr, size);
@@ -369,10 +351,14 @@ int xs_cmp(const xs_val *v1, const xs_val *v2)
xs_val *xs_dup(const xs_val *data)
/* creates a duplicate of data */
{
- int sz = xs_size(data);
- xs_val *s = xs_realloc(NULL, _xs_blk_size(sz));
+ xs_val *s = NULL;
+
+ if (data) {
+ int sz = xs_size(data);
+ s = xs_realloc(NULL, _xs_blk_size(sz));
- memcpy(s, data, sz);
+ memcpy(s, data, sz);
+ }
return s;
}
@@ -437,6 +423,39 @@ xs_val *xs_insert_m(xs_val *data, int offset, const char *mem, int size)
}
+xs_val *xs_stock(int type)
+/* returns stock values */
+{
+ static xs_val stock_null[] = { XSTYPE_NULL };
+ static xs_val stock_true[] = { XSTYPE_TRUE };
+ static xs_val stock_false[] = { XSTYPE_FALSE };
+ static xs_val stock_0[] = { XSTYPE_NUMBER, '0', '\0' };
+ static xs_val stock_1[] = { XSTYPE_NUMBER, '1', '\0' };
+ static xs_list *stock_list = NULL;
+ static xs_dict *stock_dict = NULL;
+
+ switch (type) {
+ case 0: return stock_0;
+ case 1: return stock_1;
+ case XSTYPE_NULL: return stock_null;
+ case XSTYPE_TRUE: return stock_true;
+ case XSTYPE_FALSE: return stock_false;
+
+ case XSTYPE_LIST:
+ if (stock_list == NULL)
+ stock_list = xs_list_new();
+ return stock_list;
+
+ case XSTYPE_DICT:
+ if (stock_dict == NULL)
+ stock_dict = xs_dict_new();
+ return stock_dict;
+ }
+
+ return NULL;
+}
+
+
/** strings **/
xs_str *xs_str_new(const char *str)
@@ -647,10 +666,14 @@ xs_str *xs_tolower_i(xs_str *str)
xs_list *xs_list_new(void)
/* creates a new list */
{
- return memcpy(
- xs_realloc(NULL, _xs_blk_size(sizeof(xs_stock_list))),
- xs_stock_list, sizeof(xs_stock_list)
- );
+ int sz = 1 + _XS_TYPE_SIZE + 1;
+ xs_list *l = xs_realloc(NULL, sz);
+ memset(l, '\0', sz);
+
+ l[0] = XSTYPE_LIST;
+ _xs_put_size(&l[1], sz);
+
+ return l;
}
@@ -660,8 +683,8 @@ xs_list *_xs_list_write_litem(xs_list *list, int offset, const char *mem, int ds
XS_ASSERT_TYPE(list, XSTYPE_LIST);
if (mem == NULL) {
- mem = xs_stock_null;
- dsz = sizeof(xs_stock_null);
+ mem = xs_stock(XSTYPE_NULL);
+ dsz = xs_size(mem);
}
list = xs_expand(list, offset, dsz + 1);
@@ -947,10 +970,14 @@ xs_list *xs_list_cat(xs_list *l1, const xs_list *l2)
xs_dict *xs_dict_new(void)
/* creates a new dict */
{
- return memcpy(
- xs_realloc(NULL, _xs_blk_size(sizeof(xs_stock_dict))),
- xs_stock_dict, sizeof(xs_stock_dict)
- );
+ int sz = 1 + _XS_TYPE_SIZE + 1;
+ xs_dict *d = xs_realloc(NULL, sz);
+ memset(d, '\0', sz);
+
+ d[0] = XSTYPE_DICT;
+ _xs_put_size(&d[1], sz);
+
+ return d;
}
@@ -962,8 +989,8 @@ xs_dict *_xs_dict_write_ditem(xs_dict *dict, int offset, const xs_str *key,
XS_ASSERT_TYPE(key, XSTYPE_STRING);
if (data == NULL) {
- data = xs_stock_null;
- dsz = sizeof(xs_stock_null);
+ data = xs_stock(XSTYPE_NULL);
+ dsz = xs_size(data);
}
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);
xs_val *xs_json_loads(const xs_str *json);
xstype xs_json_load_type(FILE *f);
-int xs_json_load_array_iter(FILE *f, xs_val **value, int *c);
-int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c);
+int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c);
+int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c);
+xs_list *xs_json_load_array(FILE *f);
+xs_dict *xs_json_load_object(FILE *f);
#ifdef XS_IMPLEMENTATION
@@ -324,10 +326,9 @@ static xs_val *_xs_json_load_lexer(FILE *f, js_type *t)
}
-static xs_list *_xs_json_load_array(FILE *f);
-static xs_dict *_xs_json_load_object(FILE *f);
-
-int xs_json_load_array_iter(FILE *f, xs_val **value, int *c)
+int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c)
+/* loads the next scalar value from the JSON stream */
+/* if the value ahead is compound, value is NULL and pt is set */
{
js_type t;
@@ -346,14 +347,16 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, int *c)
return -1;
}
- if (t == JS_OBRACK)
- *value = _xs_json_load_array(f);
- else
- if (t == JS_OCURLY)
- *value = _xs_json_load_object(f);
-
- if (*value == NULL)
- return -1;
+ if (*value == NULL) {
+ /* possible compound type ahead */
+ if (t == JS_OBRACK)
+ *pt = XSTYPE_LIST;
+ else
+ if (t == JS_OCURLY)
+ *pt = XSTYPE_DICT;
+ else
+ return -1;
+ }
*c = *c + 1;
@@ -361,21 +364,38 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, int *c)
}
-static xs_list *_xs_json_load_array(FILE *f)
-/* parses a JSON array */
+xs_list *xs_json_load_array(FILE *f)
+/* loads a full JSON array (after the initial OBRACK) */
{
+ xstype t;
xs_list *l = xs_list_new();
int c = 0;
for (;;) {
xs *v = NULL;
- int r = xs_json_load_array_iter(f, &v, &c);
+ int r = xs_json_load_array_iter(f, &v, &t, &c);
if (r == -1)
l = xs_free(l);
- if (r == 1)
+ if (r == 1) {
+ /* partial load? */
+ if (v == NULL) {
+ if (t == XSTYPE_LIST)
+ v = xs_json_load_array(f);
+ else
+ if (t == XSTYPE_DICT)
+ v = xs_json_load_object(f);
+ }
+
+ /* still null? fail */
+ if (v == NULL) {
+ l = xs_free(l);
+ break;
+ }
+
l = xs_list_append(l, v);
+ }
else
break;
}
@@ -384,7 +404,9 @@ static xs_list *_xs_json_load_array(FILE *f)
}
-int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c)
+int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c)
+/* loads the next key and scalar value from the JSON stream */
+/* if the value ahead is compound, value is NULL and pt is set */
{
js_type t;
@@ -413,14 +435,16 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c)
*value = _xs_json_load_lexer(f, &t);
- if (t == JS_OBRACK)
- *value = _xs_json_load_array(f);
- else
- if (t == JS_OCURLY)
- *value = _xs_json_load_object(f);
-
- if (*value == NULL)
- return -1;
+ if (*value == NULL) {
+ /* possible complex type ahead */
+ if (t == JS_OBRACK)
+ *pt = XSTYPE_LIST;
+ else
+ if (t == JS_OCURLY)
+ *pt = XSTYPE_DICT;
+ else
+ return -1;
+ }
*c = *c + 1;
@@ -428,22 +452,39 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c)
}
-static xs_dict *_xs_json_load_object(FILE *f)
-/* parses a JSON object */
+xs_dict *xs_json_load_object(FILE *f)
+/* loads a full JSON object (after the initial OCURLY) */
{
+ xstype t;
xs_dict *d = xs_dict_new();
int c = 0;
for (;;) {
xs *k = NULL;
xs *v = NULL;
- int r = xs_json_load_object_iter(f, &k, &v, &c);
+ int r = xs_json_load_object_iter(f, &k, &v, &t, &c);
if (r == -1)
d = xs_free(d);
- if (r == 1)
+ if (r == 1) {
+ /* partial load? */
+ if (v == NULL) {
+ if (t == XSTYPE_LIST)
+ v = xs_json_load_array(f);
+ else
+ if (t == XSTYPE_DICT)
+ v = xs_json_load_object(f);
+ }
+
+ /* still null? fail */
+ if (v == NULL) {
+ d = xs_free(d);
+ break;
+ }
+
d = xs_dict_append(d, k, v);
+ }
else
break;
}
@@ -492,10 +533,10 @@ xs_val *xs_json_load(FILE *f)
xstype t = xs_json_load_type(f);
if (t == XSTYPE_LIST)
- v = _xs_json_load_array(f);
+ v = xs_json_load_array(f);
else
if (t == XSTYPE_DICT)
- v = _xs_json_load_object(f);
+ v = xs_json_load_object(f);
return v;
}
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)
const char *ext = strrchr(file, '.');
if (ext) {
- const char **p = xs_mime_types;
- xs *uext = xs_tolower_i(xs_dup(ext + 1));
+ xs *uext = xs_tolower_i(xs_dup(ext + 1));
+ int b = 0;
+ int t = xs_countof(xs_mime_types) / 2 - 2;
- while (*p) {
- int c;
+ while (t >= b) {
+ int n = (b + t) / 2;
+ const char *p = xs_mime_types[n * 2];
- if ((c = strcmp(*p, uext)) == 0)
- return p[1];
+ int c = strcmp(uext, p);
+
+ if (c < 0)
+ t = n - 1;
else
if (c > 0)
- break;
-
- p += 2;
+ b = n + 1;
+ else
+ return xs_mime_types[(n * 2) + 1];
}
}
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 @@
#ifdef XS_IMPLEMENTATION
-#ifndef countof
-#define countof(a) (sizeof((a)) / sizeof((*a)))
+#ifndef xs_countof
+#define xs_countof(a) (sizeof((a)) / sizeof((*a)))
#endif
int _xs_utf8_enc(char buf[4], unsigned int cpoint)
@@ -125,7 +125,7 @@ int xs_unicode_width(unsigned int cpoint)
/* returns the width in columns of a Unicode codepoint (somewhat simplified) */
{
int b = 0;
- int t = countof(xs_unicode_width_table) / 3 - 1;
+ int t = xs_countof(xs_unicode_width_table) / 3 - 1;
while (t >= b) {
int n = (b + t) / 2;
@@ -193,7 +193,7 @@ unsigned int *_xs_unicode_upper_search(unsigned int cpoint)
/* searches for an uppercase codepoint in the case fold table */
{
int b = 0;
- int t = countof(xs_unicode_case_fold_table) / 2 + 1;
+ int t = xs_countof(xs_unicode_case_fold_table) / 2 + 1;
while (t >= b) {
int n = (b + t) / 2;
@@ -216,7 +216,7 @@ unsigned int *_xs_unicode_lower_search(unsigned int cpoint)
/* searches for a lowercase codepoint in the case fold table */
{
unsigned int *p = xs_unicode_case_fold_table;
- unsigned int *e = p + countof(xs_unicode_case_fold_table);
+ unsigned int *e = p + xs_countof(xs_unicode_case_fold_table);
while (p < e) {
if (cpoint == p[1])
@@ -251,7 +251,7 @@ int xs_unicode_nfd(unsigned int cpoint, unsigned int *base, unsigned int *diac)
/* applies unicode Normalization Form D */
{
int b = 0;
- int t = countof(xs_unicode_nfd_table) / 3 - 1;
+ int t = xs_countof(xs_unicode_nfd_table) / 3 - 1;
while (t >= b) {
int n = (b + t) / 2;
@@ -279,7 +279,7 @@ int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint)
/* applies unicode Normalization Form C */
{
unsigned int *p = xs_unicode_nfd_table;
- unsigned int *e = p + countof(xs_unicode_nfd_table);
+ unsigned int *e = p + xs_countof(xs_unicode_nfd_table);
while (p < e) {
if (p[1] == base && p[2] == diac) {
@@ -298,7 +298,7 @@ int xs_unicode_is_alpha(unsigned int cpoint)
/* checks if a codepoint is an alpha (i.e. a letter) */
{
int b = 0;
- int t = countof(xs_unicode_alpha_table) / 2 - 1;
+ int t = xs_countof(xs_unicode_alpha_table) / 2 - 1;
while (t >= b) {
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)
l = args;
while (xs_list_iter(&l, &v)) {
- xs *kv = xs_split_n(v, "=", 2);
+ xs *kv = xs_split_n(v, "=", 1);
if (xs_list_len(kv) == 2) {
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 */