Jelajahi Sumber

Merge pull request 'master' (#1) from grunfink/snac2:master into master

Reviewed-on: https://codeberg.org/poesty/snac2/pulls/1
poesty 3 tahun lalu
induk
melakukan
e65760a349
16 mengubah file dengan 610 tambahan dan 147 penghapusan
  1. 2 2
      Makefile
  2. 10 0
      RELEASE_NOTES.md
  3. 47 9
      activitypub.c
  4. 110 11
      data.c
  5. 30 0
      doc/snac.1
  6. 1 1
      examples/docker-entrypoint.sh
  7. 16 11
      html.c
  8. 1 1
      http.c
  9. 7 1
      httpd.c
  10. 22 0
      main.c
  11. 121 80
      mastoapi.c
  12. 18 13
      snac.h
  13. 5 3
      webfinger.c
  14. 8 4
      xs.h
  15. 211 10
      xs_encdec.h
  16. 1 1
      xs_version.h

+ 2 - 2
Makefile

@@ -1,6 +1,6 @@
 PREFIX=/usr/local
 PREFIX=/usr/local
 PREFIX_MAN=$(PREFIX)/man
 PREFIX_MAN=$(PREFIX)/man
-CFLAGS?=-g -Wall
+CFLAGS?=-g -Wall -Wextra
 
 
 all: snac
 all: snac
 
 
@@ -40,7 +40,7 @@ httpd.o: httpd.c xs.h xs_io.h xs_encdec.h xs_json.h xs_socket.h \
  xs_httpd.h xs_mime.h snac.h
  xs_httpd.h xs_mime.h snac.h
 main.o: main.c xs.h xs_io.h xs_encdec.h xs_json.h snac.h
 main.o: main.c xs.h xs_io.h xs_encdec.h xs_json.h snac.h
 mastoapi.o: mastoapi.c xs.h xs_encdec.h xs_openssl.h xs_json.h xs_io.h \
 mastoapi.o: mastoapi.c xs.h xs_encdec.h xs_openssl.h xs_json.h xs_io.h \
- xs_time.h snac.h
+ xs_time.h xs_glob.h snac.h
 snac.o: snac.c xs.h xs_io.h xs_encdec.h xs_json.h xs_curl.h xs_openssl.h \
 snac.o: snac.c xs.h xs_io.h xs_encdec.h xs_json.h xs_curl.h xs_openssl.h \
  xs_socket.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h \
  xs_socket.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h \
  snac.h
  snac.h

+ 10 - 0
RELEASE_NOTES.md

@@ -1,5 +1,15 @@
 # Release Notes
 # Release Notes
 
 
+## 2.30
+
+Fixed a bug that made some notifications to be missed.
+
+New Mastodon API features: the instance public timeline is now a real one, unfavourite / unreblog is supported (somewhat). Some regression bugs regarding image posting were also fixed.
+
+The non-standard `Ping` and `Pong` #ActivityPub activities have been implemented as proposed by @tedu@honk.tedunangst.com in the https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt document (with a minor diversion: retries are managed in the same way as the rest of #snac messages).
+
+The build process now includes the `-Wextra` flag.
+
 ## 2.29
 ## 2.29
 
 
 New Mastodon API features: account search, relationships (so the Follow/Unfollow buttons now appear for each account), follow and unfollow accounts, an instance-level timeline (very kludgy), custom emojis for accounts and statuses, many bug fixes (sadly, the Mastodon official app still does not work).
 New Mastodon API features: account search, relationships (so the Follow/Unfollow buttons now appear for each account), follow and unfollow accounts, an instance-level timeline (very kludgy), custom emojis for accounts and statuses, many bug fixes (sadly, the Mastodon official app still does not work).

+ 47 - 9
activitypub.c

@@ -109,7 +109,7 @@ int actor_request(snac *snac, const char *actor, xs_dict **data)
 
 
         if (valid_status(status2)) {
         if (valid_status(status2)) {
             /* renew data */
             /* renew data */
-            status = actor_add(snac, actor, payload);
+            status = actor_add(actor, payload);
 
 
             if (data != NULL) {
             if (data != NULL) {
                 *data   = payload;
                 *data   = payload;
@@ -437,7 +437,8 @@ void process_tags(snac *snac, const char *content, d_char **n_content, d_char **
 
 
 /** messages **/
 /** messages **/
 
 
-d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char *object)
+xs_dict *msg_base(snac *snac, const char *type, const char *id,
+                  const char *actor, const char *date, const char *object)
 /* creates a base ActivityPub message */
 /* creates a base ActivityPub message */
 {
 {
     xs *did       = NULL;
     xs *did       = NULL;
@@ -467,7 +468,7 @@ d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char
         }
         }
     }
     }
 
 
-    d_char *msg = xs_dict_new();
+    xs_dict *msg = xs_dict_new();
 
 
     msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams");
     msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams");
     msg = xs_dict_append(msg, "type",     type);
     msg = xs_dict_append(msg, "type",     type);
@@ -845,6 +846,28 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 }
 }
 
 
 
 
+xs_dict *msg_ping(snac *user, const char *rcpt)
+/* creates a Ping message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
+{
+    xs_dict *msg = msg_base(user, "Ping", "@dummy", user->actor, NULL, NULL);
+
+    msg = xs_dict_append(msg, "to", rcpt);
+
+    return msg;
+}
+
+
+xs_dict *msg_pong(snac *user, const char *rcpt, const char *object)
+/* creates a Pong message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
+{
+    xs_dict *msg = msg_base(user, "Pong", "@dummy", user->actor, NULL, object);
+
+    msg = xs_dict_append(msg, "to", rcpt);
+
+    return msg;
+}
+
+
 void notify(snac *snac, xs_str *type, xs_str *utype, xs_str *actor, xs_dict *msg)
 void notify(snac *snac, xs_str *type, xs_str *utype, xs_str *actor, xs_dict *msg)
 /* notifies the user of relevant events */
 /* notifies the user of relevant events */
 {
 {
@@ -1121,7 +1144,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
     else
     else
     if (strcmp(type, "Update") == 0) {
     if (strcmp(type, "Update") == 0) {
         if (strcmp(utype, "Person") == 0) {
         if (strcmp(utype, "Person") == 0) {
-            actor_add(snac, actor, xs_dict_get(msg, "object"));
+            actor_add(actor, xs_dict_get(msg, "object"));
 
 
             snac_log(snac, xs_fmt("updated actor %s", actor));
             snac_log(snac, xs_fmt("updated actor %s", actor));
         }
         }
@@ -1147,7 +1170,19 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
             snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
             snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
     }
     }
     else
     else
-        snac_debug(snac, 1, xs_fmt("process_message type '%s' ignored", type));
+    if (strcmp(type, "Pong") == 0) {
+        snac_log(snac, xs_fmt("'Pong' received from %s", actor));
+    }
+    else
+    if (strcmp(type, "Ping") == 0) {
+        snac_log(snac, xs_fmt("'Ping' requested from %s", actor));
+
+        xs *rsp = msg_pong(snac, actor, xs_dict_get(msg, "id"));
+
+        enqueue_output_by_actor(snac, rsp, actor, 0);
+    }
+    else
+        snac_debug(snac, 1, xs_fmt("process_input_message type '%s' ignored", type));
 
 
     if (do_notify) {
     if (do_notify) {
         notify(snac, type, utype, actor, msg);
         notify(snac, type, utype, actor, msg);
@@ -1384,8 +1419,9 @@ void process_queue_item(xs_dict *q_item)
         xs *headers = xs_dict_new();
         xs *headers = xs_dict_new();
         headers = xs_dict_append(headers, "content-type", "application/json");
         headers = xs_dict_append(headers, "content-type", "application/json");
 
 
-        xs *rsp  = xs_http_request("POST", url, headers,
+        xs *rsp = xs_http_request("POST", url, headers,
                                    body, strlen(body), &status, NULL, NULL, 0);
                                    body, strlen(body), &status, NULL, NULL, 0);
+        rsp = xs_free(rsp);
 
 
         srv_debug(0, xs_fmt("telegram post %d", status));
         srv_debug(0, xs_fmt("telegram post %d", status));
     }
     }
@@ -1426,7 +1462,7 @@ int process_queue(void)
 
 
 /** HTTP handlers */
 /** HTTP handlers */
 
 
-int activitypub_get_handler(d_char *req, char *q_path,
+int activitypub_get_handler(const xs_dict *req, const char *q_path,
                             char **body, int *b_size, char **ctype)
                             char **body, int *b_size, char **ctype)
 {
 {
     int status = 200;
     int status = 200;
@@ -1519,11 +1555,13 @@ int activitypub_get_handler(d_char *req, char *q_path,
 }
 }
 
 
 
 
-int activitypub_post_handler(d_char *req, char *q_path,
-                             d_char *payload, int p_size,
+int activitypub_post_handler(const xs_dict *req, const char *q_path,
+                             char *payload, int p_size,
                              char **body, int *b_size, char **ctype)
                              char **body, int *b_size, char **ctype)
 /* processes an input message */
 /* processes an input message */
 {
 {
+    (void)b_size;
+
     int status = 202; /* accepted */
     int status = 202; /* accepted */
     char *i_ctype = xs_dict_get(req, "content-type");
     char *i_ctype = xs_dict_get(req, "content-type");
     snac snac;
     snac snac;

+ 110 - 11
data.c

@@ -328,6 +328,51 @@ int index_add(const char *fn, const char *id)
 }
 }
 
 
 
 
+int index_del_md5(const char *fn, const char *md5)
+/* deletes an md5 from an index */
+{
+    int status = 404;
+    FILE *f;
+
+    pthread_mutex_lock(&data_mutex);
+
+    if ((f = fopen(fn, "r+")) != NULL) {
+        char line[256];
+
+        while (fgets(line, sizeof(line), f) != NULL) {
+            line[32] = '\0';
+
+            if (strcmp(line, md5) == 0) {
+                /* found! just rewind, overwrite it with garbage
+                   and an eventual call to index_gc() will clean it
+                   [yes: this breaks index_len()] */
+                fseek(f, -33, SEEK_CUR);
+                fwrite("-", 1, 1, f);
+                status = 200;
+
+                break;
+            }
+        }
+
+        fclose(f);
+    }
+    else
+        status = 500;
+
+    pthread_mutex_unlock(&data_mutex);
+
+    return status;
+}
+
+
+int index_del(const char *fn, const char *id)
+/* deletes an id from an index */
+{
+    xs *md5 = xs_md5_hex(id, strlen(id));
+    return index_del_md5(fn, md5);
+}
+
+
 int index_gc(const char *fn)
 int index_gc(const char *fn)
 /* garbage-collects an index, deleting objects that are not here */
 /* garbage-collects an index, deleting objects that are not here */
 {
 {
@@ -772,6 +817,23 @@ int object_admire(const char *id, const char *actor, int like)
 }
 }
 
 
 
 
+int object_unadmire(const char *id, const char *actor, int like)
+/* actor no longer likes or announces this object */
+{
+    int status;
+    xs *fn = _object_fn(id);
+
+    fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
+
+    status = index_del(fn, actor);
+
+    srv_debug(0,
+        xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
+
+    return status;
+}
+
+
 int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del)
 int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del)
 /* adds or deletes from a user cache */
 /* adds or deletes from a user cache */
 {
 {
@@ -969,8 +1031,13 @@ void timeline_update_indexes(snac *snac, const char *id)
 
 
         if (valid_status(object_get(id, &msg))) {
         if (valid_status(object_get(id, &msg))) {
             /* if its ours and is public, also store in public */
             /* if its ours and is public, also store in public */
-            if (is_msg_public(snac, msg))
+            if (is_msg_public(snac, msg)) {
                 object_user_cache_add(snac, id, "public");
                 object_user_cache_add(snac, id, "public");
+
+                /* also add it to the instance public timeline */
+                xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
+                index_add(ipt, id);
+            }
         }
         }
     }
     }
 }
 }
@@ -1041,7 +1108,7 @@ xs_list *timeline_top_level(snac *snac, xs_list *list)
 }
 }
 
 
 
 
-d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show)
+xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show)
 /* returns a timeline (with all entries) */
 /* returns a timeline (with all entries) */
 {
 {
     int c_max;
     int c_max;
@@ -1059,7 +1126,7 @@ d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int sho
 }
 }
 
 
 
 
-d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show)
+xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
 /* returns a timeline (only top level entries) */
 /* returns a timeline (only top level entries) */
 {
 {
     xs *list = timeline_simple_list(snac, idx_name, skip, show);
     xs *list = timeline_simple_list(snac, idx_name, skip, show);
@@ -1068,6 +1135,15 @@ d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show)
 }
 }
 
 
 
 
+xs_list *timeline_instance_list(int skip, int show)
+/* returns the timeline for the full instance */
+{
+    xs *idx = xs_fmt("%s/public.idx", srv_basedir);
+
+    return index_list_desc(idx, skip, show);
+}
+
+
 /** following **/
 /** following **/
 
 
 /* this needs special treatment and cannot use the object db as is,
 /* this needs special treatment and cannot use the object db as is,
@@ -1273,30 +1349,49 @@ int is_hidden(snac *snac, const char *id)
 }
 }
 
 
 
 
-int actor_add(snac *snac, const char *actor, d_char *msg)
+int actor_add(const char *actor, xs_dict *msg)
 /* adds an actor */
 /* adds an actor */
 {
 {
     return object_add_ow(actor, msg);
     return object_add_ow(actor, msg);
 }
 }
 
 
 
 
-int actor_get(snac *snac, const char *actor, d_char **data)
+int actor_get(snac *snac1, const char *actor, xs_dict **data)
 /* returns an already downloaded actor */
 /* returns an already downloaded actor */
 {
 {
     int status = 200;
     int status = 200;
-    d_char *d;
+    xs_dict *d = NULL;
 
 
-    if (strcmp(actor, snac->actor) == 0) {
+    if (strcmp(actor, snac1->actor) == 0) {
         /* this actor */
         /* this actor */
         if (data)
         if (data)
-            *data = msg_actor(snac);
+            *data = msg_actor(snac1);
 
 
         return status;
         return status;
     }
     }
 
 
+    if (xs_startswith(actor, srv_baseurl)) {
+        /* it's a (possible) local user */
+        xs *l = xs_split(actor, "/");
+        const char *uid = xs_list_get(l, -1);
+        snac user;
+
+        if (!xs_is_null(uid) && user_open(&user, uid)) {
+            if (data)
+                *data = msg_actor(&user);
+
+            user_free(&user);
+            return 200;
+        }
+        else
+            return 404;
+    }
+
     /* read the object */
     /* read the object */
-    if (!valid_status(status = object_get(actor, &d)))
+    if (!valid_status(status = object_get(actor, &d))) {
+        d = xs_free(d);
         return status;
         return status;
+    }
 
 
     if (data)
     if (data)
         *data = d;
         *data = d;
@@ -1719,7 +1814,7 @@ static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
 }
 }
 
 
 
 
-void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries)
+void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
 /* enqueues an input message */
 /* enqueues an input message */
 {
 {
     xs *qmsg   = _new_qmsg("input", msg, retries);
     xs *qmsg   = _new_qmsg("input", msg, retries);
@@ -2020,7 +2115,11 @@ void purge_server(void)
     xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
     xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
     _purge_dir(ib_dir, 7);
     _purge_dir(ib_dir, 7);
 
 
-    srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d)", cnt, icnt));
+    /* purge the instance timeline */
+    xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
+    int itl_gc = index_gc(itl_fn);
+
+    srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d, itl: %d)", cnt, icnt, itl_gc));
 }
 }
 
 
 
 

+ 30 - 0
doc/snac.1

@@ -220,6 +220,36 @@ Please take note that they will show your timeline in a 'Mastodon fashion'
 post display with the most active threads at the top that the web interface of
 post display with the most active threads at the top that the web interface of
 .Nm
 .Nm
 provides.
 provides.
+.Ss Implementing post bots
+.Nm
+makes very easy to post messages in a non-interactive manner. This example
+posts a string:
+.Bd -literal -offset indent
+uptime | snac note $SNAC_BASEDIR $SNAC_USER -
+.Ed
+.Pp
+You can setup a line like this from a
+.Xr crontab 5
+or similar. Take note that you need a) command-line access to the same machine
+that hosts the
+.Nm
+instance, and b) write permissions to the storage directories and files.
+.Pp
+You can also post non-interactively using the Mastodon API and a command-line
+http tool like
+.Xr curl 1
+or similar. This has the advantage that you can do it remotely from any host,
+anywhere; the only thing you need is an API Token. This is an example:
+.Bd -literal -offset indent
+curl -X POST https://$SNAC_HOST/api/v1/statuses \\
+--header "Authorization: Bearer ${TOKEN}" -d "status=$(uptime)"
+.Ed
+.Pp
+You can obtain an API Token by connecting to the following URL:
+.Bd -literal -offset indent
+https://$SNAC_HOST/oauth/x-snac-get-token
+.Ed
+.Pp
 .Sh ENVIRONMENT
 .Sh ENVIRONMENT
 .Bl -tag -width Ds
 .Bl -tag -width Ds
 .It Ev DEBUG
 .It Ev DEBUG

+ 1 - 1
examples/docker-entrypoint.sh

@@ -1,7 +1,7 @@
 #! /bin/sh
 #! /bin/sh
 if [ ! -e /data/data/server.json ]
 if [ ! -e /data/data/server.json ]
 then
 then
-    echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n" | snac init /data/data
+    echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n\r\n" | snac init /data/data
     snac adduser /data/data testuser
     snac adduser /data/data testuser
 fi
 fi
 SSLKEYLOGFILE=/data/key snac httpd /data/data
 SSLKEYLOGFILE=/data/key snac httpd /data/data

+ 16 - 11
html.c

@@ -79,7 +79,7 @@ xs_str *actor_name(xs_dict *actor)
 }
 }
 
 
 
 
-d_char *html_actor_icon(snac *snac, d_char *os, char *actor,
+xs_str *html_actor_icon(xs_str *os, char *actor,
     const char *date, const char *udate, const char *url, int priv)
     const char *date, const char *udate, const char *url, int priv)
 {
 {
     xs *s = xs_str_new(NULL);
     xs *s = xs_str_new(NULL);
@@ -168,7 +168,7 @@ d_char *html_msg_icon(snac *snac, d_char *os, char *msg)
         date  = xs_dict_get(msg, "published");
         date  = xs_dict_get(msg, "published");
         udate = xs_dict_get(msg, "updated");
         udate = xs_dict_get(msg, "updated");
 
 
-        os = html_actor_icon(snac, os, actor, date, udate, url, priv);
+        os = html_actor_icon(os, actor, date, udate, url, priv);
     }
     }
 
 
     return os;
     return os;
@@ -983,7 +983,7 @@ d_char *html_entry(snac *snac, d_char *os, char *msg, int local,
 }
 }
 
 
 
 
-d_char *html_user_footer(snac *snac, d_char *s)
+xs_str *html_user_footer(xs_str *s)
 {
 {
     xs *s1 = xs_fmt(
     xs *s1 = xs_fmt(
         "<div class=\"snac-footer\">\n"
         "<div class=\"snac-footer\">\n"
@@ -1064,7 +1064,7 @@ d_char *html_timeline(snac *snac, char *list, int local, int skip, int show, int
         s = xs_str_cat(s, s1);
         s = xs_str_cat(s, s1);
     }
     }
 
 
-    s = html_user_footer(snac, s);
+    s = html_user_footer(s);
 
 
     s = xs_str_cat(s, "</body>\n</html>\n");
     s = xs_str_cat(s, "</body>\n</html>\n");
 
 
@@ -1088,8 +1088,7 @@ d_char *html_people_list(snac *snac, d_char *os, d_char *list, const char *heade
         if (valid_status(actor_get(snac, actor_id, &actor))) {
         if (valid_status(actor_get(snac, actor_id, &actor))) {
             s = xs_str_cat(s, "<div class=\"snac-post\">\n");
             s = xs_str_cat(s, "<div class=\"snac-post\">\n");
 
 
-            s = html_actor_icon(snac, s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0);
-
+            s = html_actor_icon(s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0);
 
 
             /* content (user bio) */
             /* content (user bio) */
             char *c = xs_dict_get(actor, "summary");
             char *c = xs_dict_get(actor, "summary");
@@ -1182,7 +1181,7 @@ d_char *html_people(snac *snac)
 
 
     s = html_people_list(snac, s, wers, L("People that follows you"), "e");
     s = html_people_list(snac, s, wers, L("People that follows you"), "e");
 
 
-    s = html_user_footer(snac, s);
+    s = html_user_footer(s);
 
 
     s = xs_str_cat(s, "</body>\n</html>\n");
     s = xs_str_cat(s, "</body>\n</html>\n");
 
 
@@ -1226,7 +1225,7 @@ xs_str *html_notifications(snac *snac)
         const char *actor_id = xs_dict_get(noti, "actor");
         const char *actor_id = xs_dict_get(noti, "actor");
         xs *actor = NULL;
         xs *actor = NULL;
 
 
-        if (!valid_status(object_get(actor_id, &actor)))
+        if (!valid_status(actor_get(snac, actor_id, &actor)))
             continue;
             continue;
 
 
         xs *a_name = actor_name(actor);
         xs *a_name = actor_name(actor);
@@ -1277,12 +1276,13 @@ xs_str *html_notifications(snac *snac)
          s = xs_str_cat(s, s1);
          s = xs_str_cat(s, s1);
     }
     }
 
 
-    s = html_user_footer(snac, s);
+    s = html_user_footer(s);
 
 
     s = xs_str_cat(s, "</body>\n</html>\n");
     s = xs_str_cat(s, "</body>\n</html>\n");
 
 
     /* set the check time to now */
     /* set the check time to now */
     xs *dummy = notify_check_time(snac, 1);
     xs *dummy = notify_check_time(snac, 1);
+    dummy = xs_free(dummy);
 
 
     timeline_touch(snac);
     timeline_touch(snac);
 
 
@@ -1290,7 +1290,8 @@ xs_str *html_notifications(snac *snac)
 }
 }
 
 
 
 
-int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype)
+int html_get_handler(const xs_dict *req, const char *q_path,
+                     char **body, int *b_size, char **ctype)
 {
 {
     char *accept = xs_dict_get(req, "accept");
     char *accept = xs_dict_get(req, "accept");
     int status = 404;
     int status = 404;
@@ -1547,9 +1548,13 @@ int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char *
 }
 }
 
 
 
 
-int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size,
+int html_post_handler(const xs_dict *req, const char *q_path,
+                      char *payload, int p_size,
                       char **body, int *b_size, char **ctype)
                       char **body, int *b_size, char **ctype)
 {
 {
+    (void)p_size;
+    (void)ctype;
+
     int status = 0;
     int status = 0;
     snac snac;
     snac snac;
     char *uid, *p_path;
     char *uid, *p_path;

+ 1 - 1
http.c

@@ -33,7 +33,7 @@ xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
     date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT");
     date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT");
 
 
     {
     {
-        xs *s = xs_replace(url, "https:/" "/", "");
+        xs *s = xs_replace_n(url, "https:/" "/", "", 1);
         l1 = xs_split_n(s, "/", 1);
         l1 = xs_split_n(s, "/", 1);
     }
     }
 
 

+ 7 - 1
httpd.c

@@ -43,12 +43,14 @@ d_char *nodeinfo_2_0(void)
 }
 }
 
 
 
 
-int server_get_handler(d_char *req, char *q_path,
+int server_get_handler(xs_dict *req, char *q_path,
                        char **body, int *b_size, char **ctype)
                        char **body, int *b_size, char **ctype)
 /* basic server services */
 /* basic server services */
 {
 {
     int status = 0;
     int status = 0;
 
 
+    (void)req;
+
     /* is it the server root? */
     /* is it the server root? */
     if (*q_path == '\0') {
     if (*q_path == '\0') {
         /* try to open greeting.html */
         /* try to open greeting.html */
@@ -285,6 +287,8 @@ static jmp_buf on_break;
 
 
 void term_handler(int s)
 void term_handler(int s)
 {
 {
+    (void)s;
+
     longjmp(on_break, 1);
     longjmp(on_break, 1);
 }
 }
 
 
@@ -401,6 +405,8 @@ static void *background_thread(void *arg)
 {
 {
     time_t purge_time;
     time_t purge_time;
 
 
+    (void)arg;
+
     /* first purge time */
     /* first purge time */
     purge_time = time(NULL) + 10 * 60;
     purge_time = time(NULL) + 10 * 60;
 
 

+ 22 - 0
main.c

@@ -30,6 +30,7 @@ int usage(void)
     printf("actor {basedir} {uid} {url}      Requests an actor\n");
     printf("actor {basedir} {uid} {url}      Requests an actor\n");
     printf("note {basedir} {uid} {'text'}    Sends a note to followers\n");
     printf("note {basedir} {uid} {'text'}    Sends a note to followers\n");
     printf("resetpwd {basedir} {uid}         Resets the password of a user\n");
     printf("resetpwd {basedir} {uid}         Resets the password of a user\n");
+    printf("ping {basedir} {uid} {actor}     Pings an actor\n");
 
 
     return 1;
     return 1;
 }
 }
@@ -228,6 +229,27 @@ int main(int argc, char *argv[])
         return 0;
         return 0;
     }
     }
 
 
+    if (strcmp(cmd, "ping") == 0) {
+        xs *actor_o = NULL;
+
+        if (valid_status(actor_request(&snac, url, &actor_o))) {
+            xs *msg = msg_ping(&snac, url);
+
+            enqueue_output_by_actor(&snac, msg, url, 0);
+
+            if (dbglevel) {
+                xs *j = xs_json_dumps_pp(msg, 4);
+                printf("%s\n", j);
+            }
+        }
+        else {
+            srv_log(xs_fmt("Error getting actor %s", url));
+            return 1;
+        }
+
+        return 0;
+    }
+
     if (strcmp(cmd, "request") == 0) {
     if (strcmp(cmd, "request") == 0) {
         int status;
         int status;
         xs *data = NULL;
         xs *data = NULL;

+ 121 - 80
mastoapi.c

@@ -162,7 +162,7 @@ const char *login_page = ""
 "<!DOCTYPE html>\n"
 "<!DOCTYPE html>\n"
 "<body><h1>%s OAuth identify</h1>\n"
 "<body><h1>%s OAuth identify</h1>\n"
 "<div style=\"background-color: red; color: white\">%s</div>\n"
 "<div style=\"background-color: red; color: white\">%s</div>\n"
-"<form method=\"post\" action=\"https:/" "/%s/oauth/x-snac-login\">\n"
+"<form method=\"post\" action=\"https:/" "/%s/%s\">\n"
 "<p>Login: <input type=\"text\" name=\"login\"></p>\n"
 "<p>Login: <input type=\"text\" name=\"login\"></p>\n"
 "<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
 "<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
 "<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
 "<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
@@ -175,6 +175,8 @@ const char *login_page = ""
 int oauth_get_handler(const xs_dict *req, const char *q_path,
 int oauth_get_handler(const xs_dict *req, const char *q_path,
                       char **body, int *b_size, char **ctype)
                       char **body, int *b_size, char **ctype)
 {
 {
+    (void)b_size;
+
     if (!xs_startswith(q_path, "/oauth/"))
     if (!xs_startswith(q_path, "/oauth/"))
         return 0;
         return 0;
 
 
@@ -185,7 +187,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
 
 
     int status   = 404;
     int status   = 404;
     xs_dict *msg = xs_dict_get(req, "q_vars");
     xs_dict *msg = xs_dict_get(req, "q_vars");
-    xs *cmd      = xs_replace(q_path, "/oauth", "");
+    xs *cmd      = xs_replace_n(q_path, "/oauth", "", 1);
 
 
     srv_debug(1, xs_fmt("oauth_get_handler %s", q_path));
     srv_debug(1, xs_fmt("oauth_get_handler %s", q_path));
 
 
@@ -206,17 +208,28 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
                 if (xs_is_null(state))
                 if (xs_is_null(state))
                     state = "";
                     state = "";
 
 
-                *body  = xs_fmt(login_page, host, "", host, ruri, cid, state, USER_AGENT);
+                *body  = xs_fmt(login_page, host, "", host, "oauth/x-snac-login",
+                                ruri, cid, state, USER_AGENT);
                 *ctype = "text/html";
                 *ctype = "text/html";
                 status = 200;
                 status = 200;
 
 
-                srv_debug(0, xs_fmt("oauth authorize: generating login page"));
+                srv_debug(1, xs_fmt("oauth authorize: generating login page"));
             }
             }
             else
             else
-                srv_debug(0, xs_fmt("oauth authorize: bad client_id %s", cid));
+                srv_debug(1, xs_fmt("oauth authorize: bad client_id %s", cid));
         }
         }
         else
         else
-            srv_debug(0, xs_fmt("oauth authorize: invalid or unset arguments"));
+            srv_debug(1, xs_fmt("oauth authorize: invalid or unset arguments"));
+    }
+    else
+    if (strcmp(cmd, "/x-snac-get-token") == 0) {
+        const char *host = xs_dict_get(srv_config, "host");
+
+        *body  = xs_fmt(login_page, host, "", host, "oauth/x-snac-get-token",
+                        "", "", "", USER_AGENT);
+        *ctype = "text/html";
+        status = 200;
+
     }
     }
 
 
     return status;
     return status;
@@ -227,6 +240,9 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
                        const char *payload, int p_size,
                        const char *payload, int p_size,
                        char **body, int *b_size, char **ctype)
                        char **body, int *b_size, char **ctype)
 {
 {
+    (void)p_size;
+    (void)b_size;
+
     if (!xs_startswith(q_path, "/oauth/"))
     if (!xs_startswith(q_path, "/oauth/"))
         return 0;
         return 0;
 
 
@@ -245,7 +261,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
     else
     else
         args = xs_dup(xs_dict_get(req, "p_vars"));
         args = xs_dup(xs_dict_get(req, "p_vars"));
 
 
-    xs *cmd = xs_replace(q_path, "/oauth", "");
+    xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
 
 
     srv_debug(1, xs_fmt("oauth_post_handler %s", q_path));
     srv_debug(1, xs_fmt("oauth_post_handler %s", q_path));
 
 
@@ -259,7 +275,8 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
         const char *host = xs_dict_get(srv_config, "host");
         const char *host = xs_dict_get(srv_config, "host");
 
 
         /* by default, generate another login form with an error */
         /* by default, generate another login form with an error */
-        *body  = xs_fmt(login_page, host, "LOGIN INCORRECT", host, redir, cid, state, USER_AGENT);
+        *body  = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-login",
+                        redir, cid, state, USER_AGENT);
         *ctype = "text/html";
         *ctype = "text/html";
         status = 200;
         status = 200;
 
 
@@ -268,8 +285,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 
 
             if (user_open(&snac, login)) {
             if (user_open(&snac, login)) {
                 /* check the login + password */
                 /* check the login + password */
-                if (check_password(login, passwd,
-                    xs_dict_get(snac.config, "passwd"))) {
+                if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
                     /* success! redirect to the desired uri */
                     /* success! redirect to the desired uri */
                     xs *code = random_str();
                     xs *code = random_str();
 
 
@@ -328,7 +344,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
             const char *auhdr = xs_dict_get(req, "authorization");
             const char *auhdr = xs_dict_get(req, "authorization");
 
 
             if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) {
             if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) {
-                xs *s1 = xs_replace(auhdr, "Basic ", "");
+                xs *s1 = xs_replace_n(auhdr, "Basic ", "", 1);
                 int size;
                 int size;
                 xs *s2 = xs_base64_dec(s1, &size);
                 xs *s2 = xs_base64_dec(s1, &size);
 
 
@@ -373,7 +389,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 
 
                 const char *uid = xs_dict_get(app, "uid");
                 const char *uid = xs_dict_get(app, "uid");
 
 
-                srv_debug(0, xs_fmt("oauth token: "
+                srv_debug(1, xs_fmt("oauth token: "
                                 "successful login for %s, new token %s", uid, tokid));
                                 "successful login for %s, new token %s", uid, tokid));
 
 
                 xs *token = xs_dict_new();
                 xs *token = xs_dict_new();
@@ -387,7 +403,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
             }
             }
         }
         }
         else {
         else {
-            srv_debug(0, xs_fmt("oauth token: invalid or unset arguments"));
+            srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
             status = 400;
             status = 400;
         }
         }
     }
     }
@@ -409,7 +425,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
             }
             }
             else {
             else {
                 token_del(tokid);
                 token_del(tokid);
-                srv_debug(0, xs_fmt("oauth revoke: revoked token %s", tokid));
+                srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
                 status = 200;
                 status = 200;
 
 
                 /* also delete the app, as it serves no purpose from now on */
                 /* also delete the app, as it serves no purpose from now on */
@@ -417,10 +433,52 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
             }
             }
         }
         }
         else {
         else {
-            srv_debug(0, xs_fmt("oauth revoke: invalid or unset arguments"));
+            srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
             status = 403;
             status = 403;
         }
         }
     }
     }
+    if (strcmp(cmd, "/x-snac-get-token") == 0) {
+        const char *login  = xs_dict_get(args, "login");
+        const char *passwd = xs_dict_get(args, "passwd");
+
+        const char *host = xs_dict_get(srv_config, "host");
+
+        /* by default, generate another login form with an error */
+        *body  = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-get-token",
+                        "", "", "", USER_AGENT);
+        *ctype = "text/html";
+        status = 200;
+
+        if (login && passwd) {
+            snac user;
+
+            if (user_open(&user, login)) {
+                /* check the login + password */
+                if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
+                    /* success! create a new token */
+                    xs *tokid = random_str();
+
+                    srv_debug(1, xs_fmt("x-snac-new-token: "
+                                    "successful login for %s, new token %s", login, tokid));
+
+                    xs *token = xs_dict_new();
+                    token = xs_dict_append(token, "token",         tokid);
+                    token = xs_dict_append(token, "client_id",     "snac-client");
+                    token = xs_dict_append(token, "client_secret", "");
+                    token = xs_dict_append(token, "uid",           login);
+                    token = xs_dict_append(token, "code",          "");
+
+                    token_add(tokid, token);
+
+                    *ctype = "text/plain";
+                    xs_free(*body);
+                    *body = xs_dup(tokid);
+                }
+
+                user_free(&user);
+            }
+        }
+    }
 
 
     return status;
     return status;
 }
 }
@@ -537,7 +595,6 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
     xs *f   = xs_val_new(XSTYPE_FALSE);
     xs *f   = xs_val_new(XSTYPE_FALSE);
     xs *t   = xs_val_new(XSTYPE_TRUE);
     xs *t   = xs_val_new(XSTYPE_TRUE);
     xs *n   = xs_val_new(XSTYPE_NULL);
     xs *n   = xs_val_new(XSTYPE_NULL);
-    xs *el  = xs_list_new();
     xs *idx = NULL;
     xs *idx = NULL;
     xs *ixc = NULL;
     xs *ixc = NULL;
 
 
@@ -787,7 +844,7 @@ int process_auth_token(snac *snac, const xs_dict *req)
 
 
     /* if there is an authorization field, try to validate it */
     /* if there is an authorization field, try to validate it */
     if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
     if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
-        xs *tokid = xs_replace(v, "Bearer ", "");
+        xs *tokid = xs_replace_n(v, "Bearer ", "", 1);
         xs *token = token_get(tokid);
         xs *token = token_get(tokid);
 
 
         if (token != NULL) {
         if (token != NULL) {
@@ -815,6 +872,8 @@ int process_auth_token(snac *snac, const xs_dict *req)
 int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                          char **body, int *b_size, char **ctype)
                          char **body, int *b_size, char **ctype)
 {
 {
+    (void)b_size;
+
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
         return 0;
         return 0;
 
 
@@ -826,7 +885,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
     int status    = 404;
     int status    = 404;
     xs_dict *args = xs_dict_get(req, "q_vars");
     xs_dict *args = xs_dict_get(req, "q_vars");
-    xs *cmd       = xs_replace(q_path, "/api", "");
+    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
 
 
     snac snac1 = {0};
     snac snac1 = {0};
     int logged_in = process_auth_token(&snac1, req);
     int logged_in = process_auth_token(&snac1, req);
@@ -1036,11 +1095,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/timelines/public") == 0) {
     if (strcmp(cmd, "/v1/timelines/public") == 0) {
-        /* the public timeline (public timelines for all users) */
+        /* the instance public timeline (public timelines for all users) */
 
 
-        /* this is an ugly kludge: first users in the list get all the fame */
+        /* NOTE: this api call needs no authorization; but,
+           I need a logged-in user in mastoapi_status() for
+           is_msg_public() and the liked/boosted flags,
+           so it will silently fail for pure public access */
 
 
-        const char *limit_s  = xs_dict_get(args, "limit");
+        const char *limit_s = xs_dict_get(args, "limit");
         int limit = 0;
         int limit = 0;
         int cnt   = 0;
         int cnt   = 0;
 
 
@@ -1050,44 +1112,28 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
         if (limit == 0)
         if (limit == 0)
             limit = 20;
             limit = 20;
 
 
-        xs *out    = xs_list_new();
-        xs *users  = user_list();
-        xs_list *p = users;
-        xs_str *uid;
-
-        while (xs_list_iter(&p, &uid) && cnt < limit) {
-            snac user;
-
-            if (user_open(&user, uid)) {
-                xs *timeline = timeline_simple_list(&user, "public", 0, 4);
-                xs_list *p2  = timeline;
-                xs_str *v;
-
-                while (xs_list_iter(&p2, &v) && cnt < limit) {
-                    xs *msg = NULL;
+        xs *timeline = timeline_instance_list(0, limit);
+        xs *out      = xs_list_new();
+        xs_list *p   = timeline;
+        xs_str *md5;
 
 
-                    /* get the entry */
-                    if (!valid_status(timeline_get_by_md5(&user, v, &msg)))
-                        continue;
-
-                    /* discard non-Notes */
-                    if (strcmp(xs_dict_get(msg, "type"), "Note") != 0)
-                        continue;
+        while (logged_in && xs_list_iter(&p, &md5) && cnt < limit) {
+            xs *msg = NULL;
 
 
-                    /* discard entries not by this user */
-                    if (!xs_startswith(xs_dict_get(msg, "id"), user.actor))
-                        continue;
+            /* get the entry */
+            if (!valid_status(object_get_by_md5(md5, &msg)))
+                continue;
 
 
-                    /* convert the Note into a Mastodon status */
-                    xs *st = mastoapi_status(&user, msg);
+            /* discard non-Notes */
+            if (strcmp(xs_dict_get(msg, "type"), "Note") != 0)
+                continue;
 
 
-                    if (st != NULL) {
-                        out = xs_list_append(out, st);
-                        cnt++;
-                    }
-                }
+            /* convert the Note into a Mastodon status */
+            xs *st = mastoapi_status(&snac1, msg);
 
 
-                user_free(&user);
+            if (st != NULL) {
+                out = xs_list_append(out, st);
+                cnt++;
             }
             }
         }
         }
 
 
@@ -1121,7 +1167,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                 xs *actor = NULL;
                 xs *actor = NULL;
                 xs *entry = NULL;
                 xs *entry = NULL;
 
 
-                if (!valid_status(object_get(xs_dict_get(noti, "actor"), &actor)))
+                if (!valid_status(actor_get(&snac1, xs_dict_get(noti, "actor"), &actor)))
                     continue;
                     continue;
 
 
                 if (objid != NULL && !valid_status(object_get(objid, &entry)))
                 if (objid != NULL && !valid_status(object_get(objid, &entry)))
@@ -1287,25 +1333,6 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
             cfg = xs_dict_append(cfg, "statuses", d11);
             cfg = xs_dict_append(cfg, "statuses", d11);
         }
         }
 
 
-        {
-            xs *d11 = xs_dict_new();
-            xs *mt  = xs_list_new();
-
-            mt = xs_list_append(mt, "image/jpeg");
-            mt = xs_list_append(mt, "image/png");
-            mt = xs_list_append(mt, "image/gif");
-
-            d11 = xs_dict_append(d11, "supported_mime_types", mt);
-
-            d11 = xs_dict_append(d11, "image_size_limit", z);
-            d11 = xs_dict_append(d11, "image_matrix_limit", z);
-            d11 = xs_dict_append(d11, "video_size_limit", z);
-            d11 = xs_dict_append(d11, "video_matrix_limit", z);
-            d11 = xs_dict_append(d11, "video_frame_rate_limit", z);
-
-            cfg = xs_dict_append(cfg, "media_attachments", d11);
-        }
-
         ins = xs_dict_append(ins, "configuration", cfg);
         ins = xs_dict_append(ins, "configuration", cfg);
 
 
         *body  = xs_json_dumps_pp(ins, 4);
         *body  = xs_json_dumps_pp(ins, 4);
@@ -1326,7 +1353,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
             /* skip the 'fake' part of the id */
             /* skip the 'fake' part of the id */
             id = MID_TO_MD5(id);
             id = MID_TO_MD5(id);
 
 
-            if (valid_status(timeline_get_by_md5(&snac1, id, &msg))) {
+            if (valid_status(object_get_by_md5(id, &msg))) {
                 if (op == NULL) {
                 if (op == NULL) {
                     if (!is_muted(&snac1, xs_dict_get(msg, "attributedTo"))) {
                     if (!is_muted(&snac1, xs_dict_get(msg, "attributedTo"))) {
                         /* return the status itself */
                         /* return the status itself */
@@ -1487,6 +1514,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                           const char *payload, int p_size,
                           const char *payload, int p_size,
                           char **body, int *b_size, char **ctype)
                           char **body, int *b_size, char **ctype)
 {
 {
+    (void)p_size;
+    (void)b_size;
+
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
         return 0;
         return 0;
 
 
@@ -1513,7 +1543,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
         printf("%s\n", j);
         printf("%s\n", j);
     }*/
     }*/
 
 
-    xs *cmd = xs_replace(q_path, "/api", "");
+    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
 
 
     snac snac = {0};
     snac snac = {0};
     int logged_in = process_auth_token(&snac, req);
     int logged_in = process_auth_token(&snac, req);
@@ -1562,7 +1592,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 
 
             app_add(cid, app);
             app_add(cid, app);
 
 
-            srv_debug(0, xs_fmt("mastoapi apps: new app %s", cid));
+            srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
         }
         }
     }
     }
     else
     else
@@ -1582,6 +1612,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
             if (xs_is_null(media_ids))
             if (xs_is_null(media_ids))
                 media_ids = xs_dict_get(args, "media_ids[]");
                 media_ids = xs_dict_get(args, "media_ids[]");
 
 
+            if (xs_is_null(visibility))
+                visibility = "public";
+
             xs *attach_list = xs_list_new();
             xs *attach_list = xs_list_new();
             xs *irt         = NULL;
             xs *irt         = NULL;
 
 
@@ -1683,7 +1716,11 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                     }
                     }
                     else
                     else
                     if (strcmp(op, "unfavourite") == 0) {
                     if (strcmp(op, "unfavourite") == 0) {
-                        /* snac does not support Undo+Like */
+                        /* 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);
                     }
                     }
                     else
                     else
                     if (strcmp(op, "reblog") == 0) {
                     if (strcmp(op, "reblog") == 0) {
@@ -1698,7 +1735,8 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                     }
                     }
                     else
                     else
                     if (strcmp(op, "unreblog") == 0) {
                     if (strcmp(op, "unreblog") == 0) {
-                        /* snac does not support Undo+Announce */
+                        /* partial support: see comment in 'unfavourite' */
+                        object_unadmire(id, snac.actor, 0);
                     }
                     }
                     else
                     else
                     if (strcmp(op, "bookmark") == 0) {
                     if (strcmp(op, "bookmark") == 0) {
@@ -1903,6 +1941,9 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
                           const char *payload, int p_size,
                           const char *payload, int p_size,
                           char **body, int *b_size, char **ctype)
                           char **body, int *b_size, char **ctype)
 {
 {
+    (void)p_size;
+    (void)b_size;
+
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
         return 0;
         return 0;
 
 
@@ -1924,7 +1965,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
     if (args == NULL)
     if (args == NULL)
         return 400;
         return 400;
 
 
-    xs *cmd = xs_replace(q_path, "/api", "");
+    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
 
 
     snac snac = {0};
     snac snac = {0};
     int logged_in = process_auth_token(&snac, req);
     int logged_in = process_auth_token(&snac, req);

+ 18 - 13
snac.h

@@ -1,7 +1,7 @@
 /* snac - A simple, minimalistic ActivityPub instance */
 /* snac - A simple, minimalistic ActivityPub instance */
 /* copyright (c) 2022 - 2023 grunfink / MIT license */
 /* copyright (c) 2022 - 2023 grunfink / MIT license */
 
 
-#define VERSION "2.29"
+#define VERSION "2.30"
 
 
 #define USER_AGENT "snac/" VERSION
 #define USER_AGENT "snac/" VERSION
 
 
@@ -83,6 +83,7 @@ int object_del_if_unref(const char *id);
 double object_ctime_by_md5(const char *md5);
 double object_ctime_by_md5(const char *md5);
 double object_ctime(const char *id);
 double object_ctime(const char *id);
 int object_admire(const char *id, const char *actor, int like);
 int object_admire(const char *id, const char *actor, int like);
+int object_unadmire(const char *id, const char *actor, int like);
 
 
 int object_likes_len(const char *id);
 int object_likes_len(const char *id);
 int object_announces_len(const char *id);
 int object_announces_len(const char *id);
@@ -105,14 +106,14 @@ int timeline_touch(snac *snac);
 int timeline_here(snac *snac, const char *md5);
 int timeline_here(snac *snac, const char *md5);
 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
 int timeline_del(snac *snac, char *id);
 int timeline_del(snac *snac, char *id);
-d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show);
-d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show);
+xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show);
+xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show);
 int timeline_add(snac *snac, char *id, char *o_msg);
 int timeline_add(snac *snac, char *id, char *o_msg);
 void timeline_admire(snac *snac, char *id, char *admirer, int like);
 void timeline_admire(snac *snac, char *id, char *admirer, int like);
 
 
 xs_list *timeline_top_level(snac *snac, xs_list *list);
 xs_list *timeline_top_level(snac *snac, xs_list *list);
-
-d_char *local_list(snac *snac, int max);
+xs_list *local_list(snac *snac, int max);
+xs_list *timeline_instance_list(int skip, int show);
 
 
 int following_add(snac *snac, const char *actor, const xs_dict *msg);
 int following_add(snac *snac, const char *actor, const xs_dict *msg);
 int following_del(snac *snac, const char *actor);
 int following_del(snac *snac, const char *actor);
@@ -127,8 +128,8 @@ int is_muted(snac *snac, const char *actor);
 void hide(snac *snac, const char *id);
 void hide(snac *snac, const char *id);
 int is_hidden(snac *snac, const char *id);
 int is_hidden(snac *snac, const char *id);
 
 
-int actor_add(snac *snac, const char *actor, d_char *msg);
-int actor_get(snac *snac, const char *actor, d_char **data);
+int actor_add(const char *actor, xs_dict *msg);
+int actor_get(snac *snac, const char *actor, xs_dict **data);
 
 
 int static_get(snac *snac, const char *id, d_char **data, int *size);
 int static_get(snac *snac, const char *id, d_char **data, int *size);
 void static_put(snac *snac, const char *id, const char *data, int size);
 void static_put(snac *snac, const char *id, const char *data, int size);
@@ -154,7 +155,7 @@ void inbox_add(const char *inbox);
 void inbox_add_by_actor(const xs_dict *actor);
 void inbox_add_by_actor(const xs_dict *actor);
 xs_list *inbox_list(void);
 xs_list *inbox_list(void);
 
 
-void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries);
+void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries);
 void enqueue_output_raw(const char *keyid, const char *seckey,
 void enqueue_output_raw(const char *keyid, const char *seckey,
                         xs_dict *msg, xs_str *inbox, int retries);
                         xs_dict *msg, xs_str *inbox, int retries);
 void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries);
 void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries);
@@ -186,7 +187,7 @@ int check_signature(snac *snac, xs_dict *req, xs_str **err);
 void httpd(void);
 void httpd(void);
 
 
 int webfinger_request(const char *qs, char **actor, char **user);
 int webfinger_request(const char *qs, char **actor, char **user);
-int webfinger_get_handler(d_char *req, char *q_path,
+int webfinger_get_handler(xs_dict *req, char *q_path,
                           char **body, int *b_size, char **ctype);
                           char **body, int *b_size, char **ctype);
 
 
 const char *default_avatar_base64(void);
 const char *default_avatar_base64(void);
@@ -202,6 +203,8 @@ d_char *msg_undo(snac *snac, char *object);
 d_char *msg_delete(snac *snac, char *id);
 d_char *msg_delete(snac *snac, char *id);
 d_char *msg_actor(snac *snac);
 d_char *msg_actor(snac *snac);
 xs_dict *msg_update(snac *snac, xs_dict *object);
 xs_dict *msg_update(snac *snac, xs_dict *object);
+xs_dict *msg_ping(snac *user, const char *rcpt);
+xs_dict *msg_pong(snac *user, const char *rcpt, const char *object);
 
 
 int activitypub_request(snac *snac, const char *url, xs_dict **data);
 int activitypub_request(snac *snac, const char *url, xs_dict **data);
 int actor_request(snac *snac, const char *actor, xs_dict **data);
 int actor_request(snac *snac, const char *actor, xs_dict **data);
@@ -219,17 +222,19 @@ int process_user_queue(snac *snac);
 void process_queue_item(xs_dict *q_item);
 void process_queue_item(xs_dict *q_item);
 int process_queue(void);
 int process_queue(void);
 
 
-int activitypub_get_handler(d_char *req, char *q_path,
+int activitypub_get_handler(const xs_dict *req, const char *q_path,
                             char **body, int *b_size, char **ctype);
                             char **body, int *b_size, char **ctype);
-int activitypub_post_handler(d_char *req, char *q_path,
+int activitypub_post_handler(const xs_dict *req, const char *q_path,
                              char *payload, int p_size,
                              char *payload, int p_size,
                              char **body, int *b_size, char **ctype);
                              char **body, int *b_size, char **ctype);
 
 
 d_char *not_really_markdown(const char *content);
 d_char *not_really_markdown(const char *content);
 d_char *sanitize(const char *str);
 d_char *sanitize(const char *str);
 
 
-int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype);
-int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size,
+int html_get_handler(const xs_dict *req, const char *q_path,
+                     char **body, int *b_size, char **ctype);
+int html_post_handler(const xs_dict *req, const char *q_path,
+                      char *payload, int p_size,
                       char **body, int *b_size, char **ctype);
                       char **body, int *b_size, char **ctype);
 
 
 int snac_init(const char *_basedir);
 int snac_init(const char *_basedir);

+ 5 - 3
webfinger.c

@@ -21,7 +21,7 @@ int webfinger_request(const char *qs, char **actor, char **user)
 
 
     if (xs_startswith(qs, "https:/" "/")) {
     if (xs_startswith(qs, "https:/" "/")) {
         /* actor query: pick the host */
         /* actor query: pick the host */
-        xs *s = xs_replace(qs, "https:/" "/", "");
+        xs *s = xs_replace_n(qs, "https:/" "/", "", 1);
 
 
         l = xs_split_n(s, "/", 1);
         l = xs_split_n(s, "/", 1);
 
 
@@ -74,7 +74,7 @@ int webfinger_request(const char *qs, char **actor, char **user)
             char *subject = xs_dict_get(obj, "subject");
             char *subject = xs_dict_get(obj, "subject");
 
 
             if (subject)
             if (subject)
-                *user = xs_replace(subject, "acct:", "");
+                *user = xs_replace_n(subject, "acct:", "", 1);
         }
         }
 
 
         if (actor != NULL) {
         if (actor != NULL) {
@@ -105,6 +105,8 @@ int webfinger_get_handler(d_char *req, char *q_path,
 {
 {
     int status;
     int status;
 
 
+    (void)b_size;
+
     if (strcmp(q_path, "/.well-known/webfinger") != 0)
     if (strcmp(q_path, "/.well-known/webfinger") != 0)
         return 0;
         return 0;
 
 
@@ -137,7 +139,7 @@ int webfinger_get_handler(d_char *req, char *q_path,
     else
     else
     if (xs_startswith(resource, "acct:")) {
     if (xs_startswith(resource, "acct:")) {
         /* it's an account name */
         /* it's an account name */
-        xs *an = xs_replace(resource, "acct:", "");
+        xs *an = xs_replace_n(resource, "acct:", "", 1);
         xs *l = NULL;
         xs *l = NULL;
 
 
         /* strip a possible leading @ */
         /* strip a possible leading @ */

+ 8 - 4
xs.h

@@ -65,8 +65,10 @@ xs_str *xs_str_new(const char *str);
 xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix);
 xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix);
 #define xs_str_prepend_i(str, prefix) xs_str_wrap_i(prefix, str, NULL)
 #define xs_str_prepend_i(str, prefix) xs_str_wrap_i(prefix, str, NULL)
 #define xs_str_cat(str, suffix) xs_str_wrap_i(NULL, str, suffix)
 #define xs_str_cat(str, suffix) xs_str_wrap_i(NULL, str, suffix)
-xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto);
-#define xs_replace(str, sfrom, sto) xs_replace_i(xs_dup(str), sfrom, sto)
+xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times);
+#define xs_replace_i(str, sfrom, sto) xs_replace_in(str, sfrom, sto, XS_ALL)
+#define xs_replace(str, sfrom, sto) xs_replace_in(xs_dup(str), sfrom, sto, XS_ALL)
+#define xs_replace_n(str, sfrom, sto, times) xs_replace_in(xs_dup(str), sfrom, sto, times)
 xs_str *xs_fmt(const char *fmt, ...);
 xs_str *xs_fmt(const char *fmt, ...);
 int xs_str_in(const char *haystack, const char *needle);
 int xs_str_in(const char *haystack, const char *needle);
 int _xs_startsorends(const char *str, const char *xfix, int ends);
 int _xs_startsorends(const char *str, const char *xfix, int ends);
@@ -416,7 +418,7 @@ xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix)
 }
 }
 
 
 
 
-xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
+xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times)
 /* replaces inline all sfrom with sto */
 /* replaces inline all sfrom with sto */
 {
 {
     XS_ASSERT_TYPE(str, XSTYPE_STRING);
     XS_ASSERT_TYPE(str, XSTYPE_STRING);
@@ -426,7 +428,7 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
     char *ss;
     char *ss;
     int offset = 0;
     int offset = 0;
 
 
-    while ((ss = strstr(str + offset, sfrom)) != NULL) {
+    while (times > 0 && (ss = strstr(str + offset, sfrom)) != NULL) {
         int n_offset = ss - str;
         int n_offset = ss - str;
 
 
         str = xs_collapse(str, n_offset, sfsz);
         str = xs_collapse(str, n_offset, sfsz);
@@ -434,6 +436,8 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
         memcpy(str + n_offset, sto, stsz);
         memcpy(str + n_offset, sto, stsz);
 
 
         offset = n_offset + stsz;
         offset = n_offset + stsz;
+
+        times--;
     }
     }
 
 
     return str;
     return str;

+ 211 - 10
xs_encdec.h

@@ -7,13 +7,20 @@
  xs_str *xs_hex_enc(const xs_val *data, int size);
  xs_str *xs_hex_enc(const xs_val *data, int size);
  xs_val *xs_hex_dec(const xs_str *hex, int *size);
  xs_val *xs_hex_dec(const xs_str *hex, int *size);
  int xs_is_hex(const char *str);
  int xs_is_hex(const char *str);
+ xs_str *xs_base32_enc(const xs_val *data, int sz);
+ xs_str *xs_base32hex_enc(const xs_val *data, int sz);
+ xs_val *xs_base32_dec(const xs_str *data, int *size);
+ xs_val *xs_base32hex_dec(const xs_str *data, int *size);
  xs_str *xs_base64_enc(const xs_val *data, int sz);
  xs_str *xs_base64_enc(const xs_val *data, int sz);
  xs_val *xs_base64_dec(const xs_str *data, int *size);
  xs_val *xs_base64_dec(const xs_str *data, int *size);
+ int xs_is_base64(const char *str);
  xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint);
  xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint);
 
 
 
 
 #ifdef XS_IMPLEMENTATION
 #ifdef XS_IMPLEMENTATION
 
 
+/** hex **/
+
 xs_str *xs_hex_enc(const xs_val *data, int size)
 xs_str *xs_hex_enc(const xs_val *data, int size)
 /* returns an hexdump of data */
 /* returns an hexdump of data */
 {
 {
@@ -78,16 +85,178 @@ int xs_is_hex(const char *str)
 }
 }
 
 
 
 
-xs_str *xs_base64_enc(const xs_val *data, int sz)
-/* encodes data to base64 */
+/** base32 */
+
+static char *xs_b32_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                          "234567=";
+
+static char *xs_b32hex_tbl = "0123456789"
+                             "ABCDEFGHIJKLMNOPQRSTUV=";
+
+/*
+    00000|00011|11111|12222|22223|33333|33444|44444
+*/
+
+xs_str *xs_base32_enc_tbl(const xs_val *data, int sz, const char *b32_tbl)
+/* encodes data to base32 using a table */
+{
+    xs_str *s = xs_str_new(NULL);
+    unsigned char *p;
+    int n;
+
+    p = (unsigned char *)data;
+
+    for (n = 0; n < sz; n += 5) {
+        int l = sz - n;
+        char enc[9] = "========";
+
+        enc[0] = b32_tbl[(p[n] >> 3) & 0x1f];
+
+        if (l > 1) {
+            enc[1] = b32_tbl[(p[n] << 2 | p[n + 1] >> 6) & 0x1f];
+            enc[2] = b32_tbl[(p[n + 1] >> 1) & 0x1f];
+
+            if (l > 2) {
+                enc[3] = b32_tbl[(p[n + 1] << 4 | p[n + 2] >> 4) & 0x1f];
+
+                if (l > 3) {
+                    enc[4] = b32_tbl[(p[n + 2] << 1 | p[n + 3] >> 7) & 0x1f];
+                    enc[5] = b32_tbl[(p[n + 3] >> 2) & 0x1f];
+
+                    if (l > 4) {
+                        enc[6] = b32_tbl[(p[n + 3] << 3 | p[n + 4] >> 5) & 0x1f];
+                        enc[7] = b32_tbl[(p[n + 4]) & 0x1f];
+                    }
+                    else
+                        enc[6] = b32_tbl[(p[n + 3] << 3) & 0x1f];
+                }
+                else
+                    enc[4] = b32_tbl[(p[n + 2] << 1) & 0x1f];
+            }
+            else
+                enc[3] = b32_tbl[(p[n + 1] << 4) & 0x1f];
+        }
+        else
+            enc[1] = b32_tbl[(p[n] << 2) & 0x1f];
+
+        s = xs_str_cat(s, enc);
+    }
+
+    return s;
+}
+
+
+xs_str *xs_base32_enc(const xs_val *data, int sz)
+/* encodes data to base32 */
+{
+    return xs_base32_enc_tbl(data, sz, xs_b32_tbl);
+}
+
+
+xs_str *xs_base32hex_enc(const xs_val *data, int sz)
+/* encodes data to base32 with HEX alphabet (RFC4648) */
+{
+    return xs_base32_enc_tbl(data, sz, xs_b32hex_tbl);
+}
+
+
+xs_val *xs_base32_dec_tbl(const xs_str *data, int *size, const char *b32_tbl)
+/* decodes data from base32 using a table */
+{
+    xs_val *s = NULL;
+    int sz = 0;
+    char *p;
+
+    p = (char *)data;
+
+    /* size of data must be a multiple of 8 */
+    if (strlen(p) % 8)
+        return NULL;
+
+    for (p = (char *)data; *p; p += 8) {
+        int cs[8];
+        int n;
+        unsigned char tmp[5];
+
+        for (n = 0; n < 8; n++) {
+            char *ss = strchr(b32_tbl, p[n]);
+
+            if (ss == NULL) {
+                /* not a base32 char */
+                return xs_free(s);
+            }
+
+            cs[n] = ss - b32_tbl;
+        }
+
+        n = 0;
+
+        /* #0 byte */
+        tmp[n++] = cs[0] << 3 | cs[1] >> 2;
+
+        if (cs[2] != 32) {
+            /* #1 byte */
+            tmp[n++] = (cs[1] & 0x3) << 6 | cs[2] << 1 | (cs[3] & 0x10) >> 4;
+
+            if (cs[4] != 32) {
+                /* #2 byte */
+                tmp[n++] = (cs[3] & 0xf) << 4 | cs[4] >> 1;
+
+                if (cs[5] != 32) {
+                    /* #3 byte */
+                    tmp[n++] = (cs[4] & 0x1) << 7 | cs[5] << 2 | cs[6] >> 3;
+
+                    if (cs[7] != 32) {
+                        /* #4 byte */
+                        tmp[n++] = (cs[6] & 0x7) << 5 | cs[7];
+                    }
+                }
+            }
+        }
+
+        /* must be done manually because data can be pure binary */
+        s = xs_realloc(s, _xs_blk_size(sz + n));
+        memcpy(s + sz, tmp, n);
+        sz += n;
+    }
+
+    /* asciiz it to use it as a string */
+    s = xs_realloc(s, _xs_blk_size(sz + 1));
+    s[sz] = '\0';
+
+    *size = sz;
+
+    return s;
+}
+
+
+xs_val *xs_base32_dec(const xs_str *data, int *size)
+/* decodes data from base32 */
+{
+    return xs_base32_dec_tbl(data, size, xs_b32_tbl);
+}
+
+
+xs_val *xs_base32hex_dec(const xs_str *data, int *size)
+/* decodes data from base32 with HEX alphabet (RFC4648) */
+{
+    return xs_base32_dec_tbl(data, size, xs_b32hex_tbl);
+}
+
+
+/** base64 */
+
+static char *xs_b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                          "abcdefghijklmnopqrstuvwxyz"
+                          "0123456789+/=";
+
+xs_str *xs_base64_enc_tbl(const xs_val *data, int sz, const char *b64_tbl)
+/* encodes data to base64 using a table */
 {
 {
     xs_str *s;
     xs_str *s;
     unsigned char *p;
     unsigned char *p;
     char *i;
     char *i;
     int bsz, n;
     int bsz, n;
-    static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-                           "abcdefghijklmnopqrstuvwxyz"
-                           "0123456789+/";
 
 
     bsz = ((sz + 3 - 1) / 3) * 4;
     bsz = ((sz + 3 - 1) / 3) * 4;
     i = s = xs_realloc(NULL, _xs_blk_size(bsz + 1));
     i = s = xs_realloc(NULL, _xs_blk_size(bsz + 1));
@@ -123,15 +292,19 @@ xs_str *xs_base64_enc(const xs_val *data, int sz)
 }
 }
 
 
 
 
-xs_val *xs_base64_dec(const xs_str *data, int *size)
-/* decodes data from base64 */
+xs_str *xs_base64_enc(const xs_val *data, int sz)
+/* encodes data to base64 */
+{
+    return xs_base64_enc_tbl(data, sz, xs_b64_tbl);
+}
+
+
+xs_val *xs_base64_dec_tbl(const xs_str *data, int *size, const char *b64_tbl)
+/* decodes data from base64 using a table */
 {
 {
     xs_val *s = NULL;
     xs_val *s = NULL;
     int sz = 0;
     int sz = 0;
     char *p;
     char *p;
-    static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-                           "abcdefghijklmnopqrstuvwxyz"
-                           "0123456789+/=";
 
 
     p = (char *)data;
     p = (char *)data;
 
 
@@ -184,6 +357,34 @@ xs_val *xs_base64_dec(const xs_str *data, int *size)
 }
 }
 
 
 
 
+xs_val *xs_base64_dec(const xs_str *data, int *size)
+/* decodes data from base64 */
+{
+    return xs_base64_dec_tbl(data, size, xs_b64_tbl);
+}
+
+
+int xs_is_base64_tbl(const char *str, const char *b64_tbl)
+/* returns 1 if str is a base64 string, with table */
+{
+    while (*str) {
+        if (strchr(b64_tbl, *str++) == NULL)
+            return 0;
+    }
+
+    return 1;
+}
+
+
+int xs_is_base64(const char *str)
+/* returns 1 if str is a base64 string */
+{
+    return xs_is_base64_tbl(str, xs_b64_tbl);
+}
+
+
+/** utf-8 **/
+
 xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint)
 xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint)
 /* encodes an Unicode codepoint to utf8 */
 /* encodes an Unicode codepoint to utf8 */
 {
 {

+ 1 - 1
xs_version.h

@@ -1 +1 @@
-/* a885c7cc4c8e6384ae23125ed05f434471ccc6fb */
+/* dfdd729248d7169b80cb6a7462fe6c0ba6efeb16 */