commit 84a767d

Louis Brauer  ·  2024-05-25 08:05:36 +0000 UTC
parents cf5718b, a292080
Merge pull request 'master' (#1) from grunfink/snac2:master into master

Reviewed-on: https://codeberg.org/louis77/snac2/pulls/1
30 files changed,  +2075, -994
M data.c
M html.c
M http.c
M main.c
M snac.h
M xs.h
+1, -1
1@@ -36,7 +36,7 @@ uninstall:
2 activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
3  xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h
4 data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
5- xs_set.h xs_time.h xs_regex.h snac.h
6+ xs_set.h xs_time.h xs_regex.h xs_match.h snac.h
7 format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
8  xs_time.h snac.h
9 html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
+1, -1
1@@ -38,7 +38,7 @@ uninstall:
2 activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
3  xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h
4 data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
5- xs_set.h xs_time.h xs_regex.h snac.h
6+ xs_set.h xs_time.h xs_regex.h xs_match.h snac.h
7 format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
8  xs_time.h snac.h
9 html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
+6, -1
 1@@ -58,7 +58,6 @@ Run `make` and then `make install` as root.
 2 
 3 If you're compiling on NetBSD, you should use the specific provided Makefile and run `make -f Makefile.NetBSD` and then `make -f Makefile.NetBSD install` as root.
 4 
 5-
 6 From version 2.27, `snac` includes support for the Mastodon API; if you are not interested on it, you can compile it out by running
 7 
 8 ```sh
 9@@ -71,6 +70,12 @@ If your compilation process complains about undefined references to `shm_open()`
10 make LDFLAGS=-lrt
11 ```
12 
13+If it still gives compilation errors (because your system does not implement the shared memory functions), you can fix it with
14+
15+```sh
16+make CFLAGS=-DWITHOUT_SHM
17+```
18+
19 See the administrator manual on how to proceed from here.
20 
21 ## Testing via Docker
+20, -0
 1@@ -1,9 +1,29 @@
 2 # Release Notes
 3 
 4+## 2.53
 5+
 6+New user feature to search by post content (using regular expressions) or tag.
 7+
 8+Added some (partial) support for `Event` object types.
 9+
10+Minor fixes: Allow unboosting your own posts (contributed by khm), CSS fixes for the Dillo browser (contributed by kvibber).
11+
12 ## 2.52
13 
14 Posts that were liked or boosted can now be unliked and unboosted.
15 
16+Outgoing message timeouts are no longer hardcoded and can be configured (see `snac(8)` for more information).
17+
18+Fixed a bug that caused some incorrect unfollows under special conditions (with shared inboxes enabled and users from the same instance that follow each other, the internal message distributor was confused).
19+
20+Mastodon API: Added support for lists.
21+
22+Added a header to avoid over-zealous caching in some browsers (contributed by louis77).
23+
24+Added support for running and federating inside hidden networks like Tor, I2P or Loki (contributed by iwojima).
25+
26+Fixed an error processing polls coming from Pleroma instances.
27+
28 ## 2.51
29 
30 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).
+14, -10
 1@@ -10,36 +10,34 @@ Mastodon API: fix whatever the fuck is making the official app and Megalodon to
 2 
 3 Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721
 4 
 5+Editing / Updating a post does not index newly added hashtags.
 6+
 7 ## Wishlist
 8 
 9-Implement `Group`-like accounts (i.e. an actor that boosts to their followers all posts that mention it).
10+Track 'Event' data types standardization; how to add plan-to-attend and similar activities (more info: https://event-federation.eu/)
11 
12-Integrate "Ability to federate with hidden networks" see https://codeberg.org/grunfink/snac2/issues/93
13+Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md
14+
15+Track "FEP-ef61: Portable Objects" https://codeberg.org/fediverse/fep/src/branch/main/fep/ef61/fep-ef61.md
16+
17+Implement `Group`-like accounts (i.e. an actor that boosts to their followers all posts that mention it).
18 
19 Integrate "Added handling for International Domain Names" PR https://codeberg.org/grunfink/snac2/pulls/104
20 
21 Consider adding Mastodon import functionality (for following_accounts.csv and outbox.json).
22 
23-Consider adding milter-like support to reject posts to mitigate spam.
24-
25 Do something about Akkoma and Misskey's quoted replies (they use the `quoteUrl` field instead of `inReplyTo`).
26 
27-Add more CSS classes according to https://comam.es/snac/grunfink/p/1705598619.090050
28-
29 Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details).
30 
31 Add support for /authorize_interaction (whatever it is).
32 
33 Add a list of hashtags to drop.
34 
35-Add domain/subdomain flexibility according to https://codeberg.org/grunfink/snac2/issues/3
36-
37 The 'history' pages are just monthly HTML snapshots of the local timeline. This is ok and cheap and easy, but is problematic if you e.g. intentionally delete a post because it will remain there in the history forever. If you activate local timeline purging, purged entries will remain in the history as 'ghosts', which may or may not be what the user wants.
38 
39 Implement bulleted lists. Mastodon is crap and won't show them, but other implementations (Friendica, Pleroma) will do.
40 
41-User request: "will it be possible to click on a link and instead of opening the original instance, we'll be able only to see a list of the posts of this person here in comam?. Something like Mastodon does."
42-
43 The actual storage system wastes too much disk space (lots of small files that really consume 4k of storage). Consider alternatives.
44 
45 ## Closed
46@@ -311,3 +309,9 @@ Consider implementing the rejection of activities from recently-created accounts
47 Consider discarding posts by content using string or regex to mitigate spam (2024-03-14T10:40:14+0100).
48 
49 Post edits should preserve the image and the image description somewhat (2024-03-22T09:57:18+0100).
50+
51+Integrate "Ability to federate with hidden networks" see https://codeberg.org/grunfink/snac2/issues/93
52+
53+Consider adding milter-like support to reject posts to mitigate spam (discarded; 2024-04-20T22:46:35+0200).
54+
55+Implement support for 'Event' data types. Example: https://fediversity.site/item/e9bdb383-eeb9-4d7d-b2f7-c6401267cae0 (2024-05-12T08:56:27+0200)
+206, -214
   1@@ -67,7 +67,7 @@ int activitypub_request(snac *user, const char *url, xs_dict **data)
   2     xs *response = NULL;
   3     xs *payload = NULL;
   4     int p_size;
   5-    char *ctype;
   6+    const char *ctype;
   7 
   8     *data = NULL;
   9 
  10@@ -154,20 +154,21 @@ int actor_request(snac *user, const char *actor, xs_dict **data)
  11 }
  12 
  13 
  14-char *get_atto(const xs_dict *msg)
  15+const char *get_atto(const xs_dict *msg)
  16 /* gets the attributedTo field (an actor) */
  17 {
  18-    char *actor = xs_dict_get(msg, "attributedTo");
  19+    const xs_val *actor = xs_dict_get(msg, "attributedTo");
  20 
  21     /* if the actor is a list of objects (like on Peertube videos), pick the Person */
  22     if (xs_type(actor) == XSTYPE_LIST) {
  23-        xs_list *p = actor;
  24-        xs_dict *v;
  25+        const xs_list *p = actor;
  26+        int c = 0;
  27+        const xs_dict *v;
  28         actor = NULL;
  29 
  30-        while (actor == NULL && xs_list_iter(&p, &v)) {
  31+        while (actor == NULL && xs_list_next(p, &v, &c)) {
  32             if (xs_type(v) == XSTYPE_DICT) {
  33-                char *type = xs_dict_get(v, "type");
  34+                const char *type = xs_dict_get(v, "type");
  35                 if (xs_type(type) == XSTYPE_STRING && strcmp(type, "Person") == 0) {
  36                     actor = xs_dict_get(v, "id");
  37 
  38@@ -186,12 +187,12 @@ xs_list *get_attachments(const xs_dict *msg)
  39 /* unify the garbage fire that are the attachments */
  40 {
  41     xs_list *l = xs_list_new();
  42-    xs_list *p;
  43+    const xs_list *p;
  44 
  45     /* try first the attachments list */
  46     if (!xs_is_null(p = xs_dict_get(msg, "attachment"))) {
  47         xs *attach = NULL;
  48-        xs_val *v;
  49+        const xs_val *v;
  50 
  51         /* ensure it's a list */
  52         if (xs_type(p) == XSTYPE_DICT) {
  53@@ -203,23 +204,24 @@ xs_list *get_attachments(const xs_dict *msg)
  54 
  55         if (xs_type(attach) == XSTYPE_LIST) {
  56             /* does the message have an image? */
  57-            if (xs_type(v = xs_dict_get(msg, "image")) == XSTYPE_DICT) {
  58+            const xs_dict *d = xs_dict_get(msg, "image");
  59+            if (xs_type(d) == XSTYPE_DICT) {
  60                 /* add it to the attachment list */
  61-                attach = xs_list_append(attach, v);
  62+                attach = xs_list_append(attach, d);
  63             }
  64         }
  65 
  66         /* now iterate the list */
  67-        p = attach;
  68-        while (xs_list_iter(&p, &v)) {
  69-            char *type = xs_dict_get(v, "mediaType");
  70+        int c = 0;
  71+        while (xs_list_next(attach, &v, &c)) {
  72+            const char *type = xs_dict_get(v, "mediaType");
  73             if (xs_is_null(type))
  74                 type = xs_dict_get(v, "type");
  75 
  76             if (xs_is_null(type))
  77                 continue;
  78 
  79-            char *href = xs_dict_get(v, "url");
  80+            const char *href = xs_dict_get(v, "url");
  81             if (xs_is_null(href))
  82                 href = xs_dict_get(v, "href");
  83             if (xs_is_null(href))
  84@@ -233,7 +235,7 @@ xs_list *get_attachments(const xs_dict *msg)
  85                     type = mt;
  86             }
  87 
  88-            char *name = xs_dict_get(v, "name");
  89+            const char *name = xs_dict_get(v, "name");
  90             if (xs_is_null(name))
  91                 name = xs_dict_get(msg, "name");
  92             if (xs_is_null(name))
  93@@ -252,29 +254,31 @@ xs_list *get_attachments(const xs_dict *msg)
  94     p = xs_dict_get(msg, "url");
  95 
  96     if (xs_type(p) == XSTYPE_LIST) {
  97-        char *href = NULL;
  98-        char *type = NULL;
  99-        xs_val *v;
 100+        const char *href = NULL;
 101+        const char *type = NULL;
 102+        int c = 0;
 103+        const xs_val *v;
 104 
 105-        while (href == NULL && xs_list_iter(&p, &v)) {
 106+        while (href == NULL && xs_list_next(p, &v, &c)) {
 107             if (xs_type(v) == XSTYPE_DICT) {
 108-                char *mtype = xs_dict_get(v, "type");
 109+                const char *mtype = xs_dict_get(v, "type");
 110 
 111                 if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "Link") == 0) {
 112                     mtype = xs_dict_get(v, "mediaType");
 113-                    xs_list *tag = xs_dict_get(v, "tag");
 114+                    const xs_list *tag = xs_dict_get(v, "tag");
 115 
 116                     if (xs_type(mtype) == XSTYPE_STRING &&
 117                         strcmp(mtype, "application/x-mpegURL") == 0 &&
 118                         xs_type(tag) == XSTYPE_LIST) {
 119                         /* now iterate the tag list, looking for a video URL */
 120-                        xs_dict *d;
 121+                        const xs_dict *d;
 122+                        int c = 0;
 123 
 124-                        while (href == NULL && xs_list_iter(&tag, &d)) {
 125+                        while (href == NULL && xs_list_next(tag, &d, &c)) {
 126                             if (xs_type(d) == XSTYPE_DICT) {
 127                                 if (xs_type(mtype = xs_dict_get(d, "mediaType")) == XSTYPE_STRING &&
 128                                     xs_startswith(mtype, "video/")) {
 129-                                    char *h = xs_dict_get(d, "href");
 130+                                    const char *h = xs_dict_get(d, "href");
 131 
 132                                     /* this is probably it */
 133                                     if (xs_type(h) == XSTYPE_STRING) {
 134@@ -303,7 +307,7 @@ xs_list *get_attachments(const xs_dict *msg)
 135 }
 136 
 137 
 138-int timeline_request(snac *snac, char **id, xs_str **wrk, int level)
 139+int timeline_request(snac *snac, const char **id, xs_str **wrk, int level)
 140 /* ensures that an entry and its ancestors are in the timeline */
 141 {
 142     int status = 0;
 143@@ -323,7 +327,7 @@ int timeline_request(snac *snac, char **id, xs_str **wrk, int level)
 144             status = activitypub_request(snac, *id, &msg);
 145 
 146             if (valid_status(status)) {
 147-                xs_dict *object  = msg;
 148+                const xs_dict *object = msg;
 149                 const char *type = xs_dict_get(object, "type");
 150 
 151                 /* get the id again from the object, as it may be different */
 152@@ -355,102 +359,35 @@ int timeline_request(snac *snac, char **id, xs_str **wrk, int level)
 153                         type = "(null)";
 154                 }
 155 
 156-                if (xs_match(type, "Note|Page|Article|Video")) {
 157-                    const char *actor = get_atto(object);
 158-
 159-                    if (content_check("filter_reject.txt", object))
 160+                if (xs_match(type, POSTLIKE_OBJECT_TYPE)) {
 161+                    if (content_match("filter_reject.txt", object))
 162                         snac_log(snac, xs_fmt("timeline_request rejected by content %s", nid));
 163                     else {
 164-                        /* request (and drop) the actor for this entry */
 165-                        if (!xs_is_null(actor))
 166-                            actor_request(snac, actor, NULL);
 167-
 168-                        /* does it have an ancestor? */
 169-                        char *in_reply_to = xs_dict_get(object, "inReplyTo");
 170-
 171-                        /* store */
 172-                        timeline_add(snac, nid, object);
 173-
 174-                        /* recurse! */
 175-                        timeline_request(snac, &in_reply_to, NULL, level + 1);
 176-                    }
 177-                }
 178-            }
 179-        }
 180-
 181-        enqueue_request_replies(snac, *id);
 182-    }
 183-
 184-    return status;
 185-}
 186+                        const char *actor = get_atto(object);
 187 
 188+                        if (!xs_is_null(actor)) {
 189+                            /* request (and drop) the actor for this entry */
 190+                            if (!valid_status(actor_request(snac, actor, NULL))) {
 191+                                /* failed? retry later */
 192+                                enqueue_actor_refresh(snac, actor, 60);
 193+                            }
 194 
 195-void timeline_request_replies(snac *user, const char *id)
 196-/* requests all replies of a message */
 197-/* FIXME: experimental -- needs more testing */
 198-{
 199-    /* FIXME: TEMPORARILY DISABLED */
 200-    /* Reason: I've found that many of the posts in the 'replies' Collection
 201-       do not have an inReplyTo field (why??? aren't they 'replies'???).
 202-       For this reason, these requested objects are not stored as children
 203-       of the original post and they are shown as out-of-context, top level posts.
 204-       This process is disabled until I find an elegant way of providing a parent
 205-       for these 'stray' children. */
 206-    return;
 207-
 208-    xs *msg = NULL;
 209-
 210-    if (!valid_status(object_get(id, &msg)))
 211-        return;
 212-
 213-    /* does it have a replies collection? */
 214-    const xs_dict *replies = xs_dict_get(msg, "replies");
 215-
 216-    if (!xs_is_null(replies)) {
 217-        const char *type  = xs_dict_get(replies, "type");
 218-        const char *first = xs_dict_get(replies, "first");
 219-
 220-        if (!xs_is_null(type) && !xs_is_null(first) && strcmp(type, "Collection") == 0) {
 221-            const char *next = xs_dict_get(first, "next");
 222-
 223-            if (!xs_is_null(next)) {
 224-                xs *rpls = NULL;
 225-                int status = activitypub_request(user, next, &rpls);
 226-
 227-                /* request the Collection of replies */
 228-                if (valid_status(status)) {
 229-                    xs_list *items = xs_dict_get(rpls, "items");
 230-
 231-                    if (xs_type(items) == XSTYPE_LIST) {
 232-                        xs_val *v;
 233-
 234-                        /* request them all */
 235-                        while (xs_list_iter(&items, &v)) {
 236-                            if (xs_type(v) == XSTYPE_DICT) {
 237-                                /* not an id, but the object itself (!) */
 238-                                const char *c_id = xs_dict_get(v, "id");
 239-
 240-                                if (!xs_is_null(id)) {
 241-                                    snac_debug(user, 0, xs_fmt("embedded reply %s", c_id));
 242+                            /* does it have an ancestor? */
 243+                            const char *in_reply_to = xs_dict_get(object, "inReplyTo");
 244 
 245-                                    object_add(c_id, v);
 246+                            /* store */
 247+                            timeline_add(snac, nid, object);
 248 
 249-                                    /* get its own children */
 250-                                    timeline_request_replies(user, v);
 251-                                }
 252-                            }
 253-                            else {
 254-                                snac_debug(user, 0, xs_fmt("request reply %s", v));
 255-                                timeline_request(user, &v, NULL, 0);
 256-                            }
 257+                            /* recurse! */
 258+                            timeline_request(snac, &in_reply_to, NULL, level + 1);
 259                         }
 260                     }
 261                 }
 262-                else
 263-                    snac_debug(user, 0, xs_fmt("replies request error %s %d", next, status));
 264             }
 265         }
 266     }
 267+
 268+    return status;
 269 }
 270 
 271 
 272@@ -476,7 +413,7 @@ int send_to_inbox(snac *snac, const xs_str *inbox, const xs_dict *msg,
 273                   xs_val **payload, int *p_size, int timeout)
 274 /* sends a message to an Inbox */
 275 {
 276-    char *seckey = xs_dict_get(snac->key, "secret");
 277+    const char *seckey = xs_dict_get(snac->key, "secret");
 278 
 279     return send_to_inbox_raw(snac->actor, seckey, inbox, msg, payload, p_size, timeout);
 280 }
 281@@ -486,7 +423,7 @@ xs_str *get_actor_inbox(const char *actor)
 282 /* gets an actor's inbox */
 283 {
 284     xs *data = NULL;
 285-    char *v = NULL;
 286+    const char *v = NULL;
 287 
 288     if (valid_status(actor_request(NULL, actor, &data))) {
 289         /* try first endpoints/sharedInbox */
 290@@ -535,17 +472,17 @@ void post_message(snac *snac, const char *actor, const xs_dict *msg)
 291 xs_list *recipient_list(snac *snac, const xs_dict *msg, int expand_public)
 292 /* returns the list of recipients for a message */
 293 {
 294-    char *to = xs_dict_get(msg, "to");
 295-    char *cc = xs_dict_get(msg, "cc");
 296+    const xs_val *to = xs_dict_get(msg, "to");
 297+    const xs_val *cc = xs_dict_get(msg, "cc");
 298     xs_set rcpts;
 299     int n;
 300 
 301     xs_set_init(&rcpts);
 302 
 303-    char *lists[] = { to, cc, NULL };
 304+    const xs_list *lists[] = { to, cc, NULL };
 305     for (n = 0; lists[n]; n++) {
 306-        char *l = lists[n];
 307-        char *v;
 308+        xs_list *l = (xs_list *)lists[n];
 309+        const char *v;
 310         xs *tl = NULL;
 311 
 312         /* if it's a string, create a list with only one element */
 313@@ -560,7 +497,7 @@ xs_list *recipient_list(snac *snac, const xs_dict *msg, int expand_public)
 314             if (expand_public && strcmp(v, public_address) == 0) {
 315                 /* iterate the followers and add them */
 316                 xs *fwers = follower_list(snac);
 317-                char *actor;
 318+                const char *actor;
 319 
 320                 char *p = fwers;
 321                 while (xs_list_iter(&p, &actor))
 322@@ -667,13 +604,13 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
 323 
 324     /* if it's a Follow, it must be explicitly for us */
 325     if (xs_match(type, "Follow")) {
 326-        char *object = xs_dict_get(c_msg, "object");
 327+        const char *object = xs_dict_get(c_msg, "object");
 328         return !xs_is_null(object) && strcmp(snac->actor, object) == 0;
 329     }
 330 
 331     /* only accept Ping directed to us */
 332     if (xs_match(type, "Ping")) {
 333-        char *dest = xs_dict_get(c_msg, "to");
 334+        const char *dest = xs_dict_get(c_msg, "to");
 335         return !xs_is_null(dest) && strcmp(snac->actor, dest) == 0;
 336     }
 337 
 338@@ -688,10 +625,10 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
 339     if (pub_msg && following_check(snac, actor))
 340         return 1;
 341 
 342-    xs_dict *msg = xs_dict_get(c_msg, "object");
 343+    const xs_dict *msg = xs_dict_get(c_msg, "object");
 344     xs *rcpts = recipient_list(snac, msg, 0);
 345     xs_list *p = rcpts;
 346-    xs_str *v;
 347+    const xs_str *v;
 348 
 349     xs *actor_followers = NULL;
 350 
 351@@ -700,8 +637,9 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
 352         xs *actor_obj = NULL;
 353 
 354         if (valid_status(object_get(actor, &actor_obj))) {
 355-            if ((v = xs_dict_get(actor_obj, "followers")))
 356-                actor_followers = xs_dup(v);
 357+            const xs_val *fw = xs_dict_get(actor_obj, "followers");
 358+            if (fw)
 359+                actor_followers = xs_dup(fw);
 360         }
 361     }
 362 
 363@@ -724,13 +662,13 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
 364     }
 365 
 366     /* accept if it's by someone we follow */
 367-    char *atto = get_atto(msg);
 368+    const char *atto = get_atto(msg);
 369 
 370     if (pub_msg && !xs_is_null(atto) && following_check(snac, atto))
 371         return 3;
 372 
 373     /* is this message a reply to another? */
 374-    char *irt = xs_dict_get(msg, "inReplyTo");
 375+    const char *irt = xs_dict_get(msg, "inReplyTo");
 376     if (!xs_is_null(irt)) {
 377         xs *r_msg = NULL;
 378 
 379@@ -755,7 +693,7 @@ xs_str *process_tags(snac *snac, const char *content, xs_list **tag)
 380     xs_list *tl = *tag;
 381     xs *split;
 382     xs_list *p;
 383-    xs_val *v;
 384+    const xs_val *v;
 385     int n = 0;
 386 
 387     /* create a default server for incomplete mentions */
 388@@ -983,8 +921,8 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor,
 389 
 390     /* telegram */
 391 
 392-    char *bot     = xs_dict_get(snac->config, "telegram_bot");
 393-    char *chat_id = xs_dict_get(snac->config, "telegram_chat_id");
 394+    const char *bot     = xs_dict_get(snac->config, "telegram_bot");
 395+    const char *chat_id = xs_dict_get(snac->config, "telegram_chat_id");
 396 
 397     if (!xs_is_null(bot) && !xs_is_null(chat_id) && *bot && *chat_id)
 398         enqueue_telegram(body, bot, chat_id);
 399@@ -997,8 +935,8 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor,
 400         objid = actor;
 401 
 402     /* ntfy */
 403-    char *ntfy_server = xs_dict_get(snac->config, "ntfy_server");
 404-    char *ntfy_token  = xs_dict_get(snac->config, "ntfy_token");
 405+    const char *ntfy_server = xs_dict_get(snac->config, "ntfy_server");
 406+    const char *ntfy_token  = xs_dict_get(snac->config, "ntfy_token");
 407 
 408     if (!xs_is_null(ntfy_server) && *ntfy_server)
 409         enqueue_ntfy(body, ntfy_server, ntfy_token);
 410@@ -1084,7 +1022,7 @@ xs_dict *msg_base(snac *snac, const char *type, const char *id,
 411 }
 412 
 413 
 414-xs_dict *msg_collection(snac *snac, char *id)
 415+xs_dict *msg_collection(snac *snac, const char *id)
 416 /* creates an empty OrderedCollection message */
 417 {
 418     xs_dict *msg = msg_base(snac, "OrderedCollection", id, NULL, NULL, NULL);
 419@@ -1098,7 +1036,7 @@ xs_dict *msg_collection(snac *snac, char *id)
 420 }
 421 
 422 
 423-xs_dict *msg_accept(snac *snac, char *object, char *to)
 424+xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to)
 425 /* creates an Accept message (as a response to a Follow) */
 426 {
 427     xs_dict *msg = msg_base(snac, "Accept", "@dummy", snac->actor, NULL, object);
 428@@ -1109,12 +1047,12 @@ xs_dict *msg_accept(snac *snac, char *object, char *to)
 429 }
 430 
 431 
 432-xs_dict *msg_update(snac *snac, xs_dict *object)
 433+xs_dict *msg_update(snac *snac, const xs_dict *object)
 434 /* creates an Update message */
 435 {
 436     xs_dict *msg = msg_base(snac, "Update", "@object", snac->actor, "@now", object);
 437 
 438-    char *type = xs_dict_get(object, "type");
 439+    const char *type = xs_dict_get(object, "type");
 440 
 441     if (strcmp(type, "Note") == 0) {
 442         msg = xs_dict_append(msg, "to", xs_dict_get(object, "to"));
 443@@ -1137,7 +1075,7 @@ xs_dict *msg_update(snac *snac, xs_dict *object)
 444 }
 445 
 446 
 447-xs_dict *msg_admiration(snac *snac, char *object, char *type)
 448+xs_dict *msg_admiration(snac *snac, const char *object, const char *type)
 449 /* creates a Like or Announce message */
 450 {
 451     xs *a_msg    = NULL;
 452@@ -1168,7 +1106,7 @@ xs_dict *msg_admiration(snac *snac, char *object, char *type)
 453 }
 454 
 455 
 456-xs_dict *msg_repulsion(snac *user, char *id, char *type)
 457+xs_dict *msg_repulsion(snac *user, const char *id, const char *type)
 458 /* creates an Undo + admiration message */
 459 {
 460     xs *a_msg    = NULL;
 461@@ -1206,7 +1144,7 @@ xs_dict *msg_actor(snac *snac)
 462     xs *kid      = NULL;
 463     xs *f_bio    = NULL;
 464     xs_dict *msg = msg_base(snac, "Person", snac->actor, NULL, NULL, NULL);
 465-    char *p;
 466+    const char *p;
 467     int n;
 468 
 469     /* change the @context (is this really necessary?) */
 470@@ -1264,11 +1202,11 @@ xs_dict *msg_actor(snac *snac)
 471     }
 472 
 473     /* add the metadata as attachments of PropertyValue */
 474-    xs_dict *metadata = xs_dict_get(snac->config, "metadata");
 475+    const xs_dict *metadata = xs_dict_get(snac->config, "metadata");
 476     if (xs_type(metadata) == XSTYPE_DICT) {
 477         xs *attach = xs_list_new();
 478-        xs_str *k;
 479-        xs_str *v;
 480+        const xs_str *k;
 481+        const xs_str *v;
 482 
 483         int c = 0;
 484         while (xs_dict_next(metadata, &k, &v, &c)) {
 485@@ -1277,7 +1215,7 @@ xs_dict *msg_actor(snac *snac)
 486             xs *k2 = encode_html(k);
 487             xs *v2 = NULL;
 488 
 489-            if (xs_startswith(v, "https:")) {
 490+            if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/")) {
 491                 xs *t = encode_html(v);
 492                 v2 = xs_fmt("<a href=\"%s\" rel=\"me\">%s</a>", t, t);
 493             }
 494@@ -1310,7 +1248,7 @@ xs_dict *msg_create(snac *snac, const xs_dict *object)
 495 /* creates a 'Create' message */
 496 {
 497     xs_dict *msg = msg_base(snac, "Create", "@wrapper", snac->actor, NULL, object);
 498-    xs_val *v;
 499+    const xs_val *v;
 500 
 501     if ((v = get_atto(object)))
 502         msg = xs_dict_append(msg, "attributedTo", v);
 503@@ -1327,7 +1265,7 @@ xs_dict *msg_create(snac *snac, const xs_dict *object)
 504 }
 505 
 506 
 507-xs_dict *msg_undo(snac *snac, char *object)
 508+xs_dict *msg_undo(snac *snac, const xs_val *object)
 509 /* creates an 'Undo' message */
 510 {
 511     xs_dict *msg = msg_base(snac, "Undo", "@object", snac->actor, "@now", object);
 512@@ -1340,7 +1278,7 @@ xs_dict *msg_undo(snac *snac, char *object)
 513 }
 514 
 515 
 516-xs_dict *msg_delete(snac *snac, char *id)
 517+xs_dict *msg_delete(snac *snac, const char *id)
 518 /* creates a 'Delete' + 'Tombstone' for a local entry */
 519 {
 520     xs *tomb = xs_dict_new();
 521@@ -1369,7 +1307,7 @@ xs_dict *msg_follow(snac *snac, const char *q)
 522 
 523     xs *url_or_uid = xs_strip_i(xs_str_new(q));
 524 
 525-    if (xs_startswith(url_or_uid, "https:/"))
 526+    if (xs_startswith(url_or_uid, "https:/") || xs_startswith(url_or_uid, "http:/"))
 527         actor = xs_dup(url_or_uid);
 528     else
 529     if (!valid_status(webfinger_request(url_or_uid, &actor, NULL)) || actor == NULL) {
 530@@ -1382,7 +1320,7 @@ xs_dict *msg_follow(snac *snac, const char *q)
 531 
 532     if (valid_status(status)) {
 533         /* check if the actor is an alias */
 534-        char *r_actor = xs_dict_get(actor_o, "id");
 535+        const char *r_actor = xs_dict_get(actor_o, "id");
 536 
 537         if (r_actor && strcmp(actor, r_actor) != 0) {
 538             snac_log(snac, xs_fmt("actor to follow is an alias %s -> %s", actor, r_actor));
 539@@ -1398,7 +1336,7 @@ xs_dict *msg_follow(snac *snac, const char *q)
 540 
 541 
 542 xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 543-                  xs_str *in_reply_to, xs_list *attach, int priv)
 544+                  const xs_str *in_reply_to, const xs_list *attach, int priv)
 545 /* creates a 'Note' message */
 546 {
 547     xs *ntid = tid(0);
 548@@ -1413,7 +1351,7 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 549     xs *atls = xs_list_new();
 550     xs_dict *msg = msg_base(snac, "Note", id, NULL, "@now", NULL);
 551     xs_list *p;
 552-    xs_val *v;
 553+    const xs_val *v;
 554 
 555     if (rcpts == NULL)
 556         to = xs_list_new();
 557@@ -1438,7 +1376,7 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 558 
 559         if (valid_status(object_get(in_reply_to, &p_msg))) {
 560             /* add this author as recipient */
 561-            char *a, *v;
 562+            const char *a, *v;
 563 
 564             if ((a = get_atto(p_msg)) && xs_list_in(to, a) == -1)
 565                 to = xs_list_append(to, a);
 566@@ -1449,7 +1387,7 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 567                 xs *actor_o = NULL;
 568 
 569                 if (xs_list_len(l) > 3 && valid_status(object_get(a, &actor_o))) {
 570-                    char *uname = xs_dict_get(actor_o, "preferredUsername");
 571+                    const char *uname = xs_dict_get(actor_o, "preferredUsername");
 572 
 573                     if (!xs_is_null(uname) && *uname) {
 574                         xs *handle = xs_fmt("@%s@%s", uname, xs_list_get(l, 2));
 575@@ -1488,7 +1426,8 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 576 
 577     /* create the attachment list, if there are any */
 578     if (!xs_is_null(attach)) {
 579-        while (xs_list_iter(&attach, &v)) {
 580+        int c = 0;
 581+        while (xs_list_next(attach, &v, &c)) {
 582             xs *d            = xs_dict_new();
 583             const char *url  = xs_list_get(v, 0);
 584             const char *alt  = xs_list_get(v, 1);
 585@@ -1511,7 +1450,7 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 586     p = tag;
 587     while (xs_list_iter(&p, &v)) {
 588         if (xs_type(v) == XSTYPE_DICT) {
 589-            char *t;
 590+            const char *t;
 591 
 592             if (!xs_is_null(t = xs_dict_get(v, "type")) && strcmp(t, "Mention") == 0) {
 593                 if (!xs_is_null(t = xs_dict_get(v, "href")))
 594@@ -1589,7 +1528,7 @@ xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
 595 
 596     xs *o = xs_list_new();
 597     xs_list *p = (xs_list *)opts;
 598-    xs_str *v;
 599+    const xs_str *v;
 600     xs *replies = xs_json_loads("{\"type\":\"Collection\",\"totalItems\":0}");
 601 
 602     xs_set_init(&seen);
 603@@ -1635,9 +1574,9 @@ int update_question(snac *user, const char *id)
 604     xs *msg   = NULL;
 605     xs *rcnt  = xs_dict_new();
 606     xs *lopts = xs_list_new();
 607-    xs_list *opts;
 608+    const xs_list *opts;
 609     xs_list *p;
 610-    xs_val *v;
 611+    const xs_val *v;
 612 
 613     /* get the object */
 614     if (!valid_status(object_get(id, &msg)))
 615@@ -1653,8 +1592,8 @@ int update_question(snac *user, const char *id)
 616         return -3;
 617 
 618     /* fill the initial count */
 619-    p = opts;
 620-    while (xs_list_iter(&p, &v)) {
 621+    int c = 0;
 622+    while (xs_list_next(opts, &v, &c)) {
 623         const char *name = xs_dict_get(v, "name");
 624         if (name) {
 625             lopts = xs_list_append(lopts, name);
 626@@ -1760,13 +1699,13 @@ int update_question(snac *user, const char *id)
 627 
 628 /** queues **/
 629 
 630-int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
 631+int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
 632 /* processes an ActivityPub message from the input queue */
 633 /* return values: -1, fatal error; 0, transient error, retry;
 634    1, processed and done; 2, propagate to users (only when no user is set) */
 635 {
 636-    char *actor = xs_dict_get(msg, "actor");
 637-    char *type  = xs_dict_get(msg, "type");
 638+    const char *actor = xs_dict_get(msg, "actor");
 639+    const char *type  = xs_dict_get(msg, "type");
 640     xs *actor_o = NULL;
 641     int a_status;
 642     int do_notify = 0;
 643@@ -1786,7 +1725,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
 644         return -1;
 645     }
 646 
 647-    char *object, *utype;
 648+    const char *object, *utype;
 649 
 650     object = xs_dict_get(msg, "object");
 651     if (object != NULL && xs_type(object) == XSTYPE_DICT)
 652@@ -1809,7 +1748,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
 653         }
 654 
 655         /* also discard if the object to be deleted is not here */
 656-        char *obj_id = object;
 657+        const char *obj_id = object;
 658         if (xs_type(obj_id) == XSTYPE_DICT)
 659             obj_id = xs_dict_get(obj_id, "id");
 660 
 661@@ -1881,7 +1820,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
 662     int min_account_age = xs_number_get(xs_dict_get(srv_config, "min_account_age"));
 663 
 664     if (min_account_age > 0) {
 665-        char *actor_date = xs_dict_get(actor_o, "published");
 666+        const char *actor_date = xs_dict_get(actor_o, "published");
 667         if (!xs_is_null(actor_date)) {
 668             time_t actor_t = xs_parse_iso_date(actor_date, 0);
 669 
 670@@ -1941,16 +1880,39 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
 671     }
 672     else
 673     if (strcmp(type, "Undo") == 0) { /** **/
 674+        const char *id = xs_dict_get(object, "object");
 675+
 676         if (xs_type(object) != XSTYPE_DICT)
 677             utype = "Follow";
 678 
 679         if (strcmp(utype, "Follow") == 0) { /** **/
 680-            if (valid_status(follower_del(snac, actor))) {
 681-                snac_log(snac, xs_fmt("no longer following us %s", actor));
 682-                do_notify = 1;
 683+            if (id && strcmp(id, snac->actor) != 0)
 684+                snac_debug(snac, 1, xs_fmt("Undo + Follow from %s not for us (%s)", actor, id));
 685+            else {
 686+                if (valid_status(follower_del(snac, actor))) {
 687+                    snac_log(snac, xs_fmt("no longer following us %s", actor));
 688+                    do_notify = 1;
 689+                }
 690+                else
 691+                    snac_log(snac, xs_fmt("error deleting follower %s", actor));
 692             }
 693-            else
 694-                snac_log(snac, xs_fmt("error deleting follower %s", actor));
 695+        }
 696+        else
 697+        if (strcmp(utype, "Like") == 0) { /** **/
 698+            int status = object_unadmire(id, actor, 1);
 699+
 700+            snac_log(snac, xs_fmt("Unlike for %s %d", id, status));
 701+        }
 702+        else
 703+        if (strcmp(utype, "Announce") == 0) { /** **/
 704+            int status = 200;
 705+
 706+            /* commented out: if a followed user boosts something that
 707+               is requested and then unboosts, the post remains here,
 708+               but with no apparent reason, and that is confusing */
 709+            //status = object_unadmire(id, actor, 0);
 710+
 711+            snac_log(snac, xs_fmt("Unboost for %s %d", id, status));
 712         }
 713         else
 714             snac_debug(snac, 1, xs_fmt("ignored 'Undo' for object type '%s'", utype));
 715@@ -1963,19 +1925,29 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
 716         }
 717 
 718         if (xs_match(utype, "Note|Article")) { /** **/
 719-            char *id          = xs_dict_get(object, "id");
 720-            char *in_reply_to = xs_dict_get(object, "inReplyTo");
 721+            const char *id          = xs_dict_get(object, "id");
 722+            const char *in_reply_to = xs_dict_get(object, "inReplyTo");
 723+            const char *atto        = get_atto(object);
 724             xs *wrk           = NULL;
 725 
 726+            if (xs_is_null(id))
 727+                snac_log(snac, xs_fmt("malformed message: no 'id' field"));
 728+            else
 729+            if (xs_is_null(atto))
 730+                snac_log(snac, xs_fmt("malformed message: no 'attributedTo' field"));
 731+            else
 732             if (!xs_is_null(in_reply_to) && is_hidden(snac, in_reply_to)) {
 733                 snac_debug(snac, 0, xs_fmt("dropped reply %s to hidden post %s", id, in_reply_to));
 734             }
 735             else {
 736-                if (content_check("filter_reject.txt", object)) {
 737+                if (content_match("filter_reject.txt", object)) {
 738                     snac_log(snac, xs_fmt("rejected by content %s", id));
 739                     return 1;
 740                 }
 741 
 742+                if (strcmp(actor, atto) != 0)
 743+                    snac_log(snac, xs_fmt("SUSPICIOUS: actor != atto (%s != %s)", actor, atto));
 744+
 745                 timeline_request(snac, &in_reply_to, &wrk, 0);
 746 
 747                 if (timeline_add(snac, id, object)) {
 748@@ -1992,14 +1964,14 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
 749         }
 750         else
 751         if (strcmp(utype, "Question") == 0) { /**  **/
 752-            char *id = xs_dict_get(object, "id");
 753+            const char *id = xs_dict_get(object, "id");
 754 
 755             if (timeline_add(snac, id, object))
 756                 snac_log(snac, xs_fmt("new 'Question' %s %s", actor, id));
 757         }
 758         else
 759         if (strcmp(utype, "Video") == 0) { /** **/
 760-            char *id = xs_dict_get(object, "id");
 761+            const char *id = xs_dict_get(object, "id");
 762 
 763             if (timeline_add(snac, id, object))
 764                 snac_log(snac, xs_fmt("new 'Video' %s %s", actor, id));
 765@@ -2080,6 +2052,9 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
 766                             snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
 767                                 actor, object));
 768 
 769+                        /* distribute the post with the actor as 'proxy' */
 770+                        list_distribute(snac, actor, a_msg);
 771+
 772                         do_notify = 1;
 773                     }
 774                     else
 775@@ -2172,7 +2147,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
 776 }
 777 
 778 
 779-int send_email(char *msg)
 780+int send_email(const char *msg)
 781 /* invoke sendmail with email headers and body in msg */
 782 {
 783     FILE *f;
 784@@ -2204,18 +2179,18 @@ int send_email(char *msg)
 785 void process_user_queue_item(snac *snac, xs_dict *q_item)
 786 /* processes an item from the user queue */
 787 {
 788-    char *type;
 789+    const char *type;
 790     int queue_retry_max = xs_number_get(xs_dict_get(srv_config, "queue_retry_max"));
 791 
 792     if ((type = xs_dict_get(q_item, "type")) == NULL)
 793         type = "output";
 794 
 795     if (strcmp(type, "message") == 0) {
 796-        xs_dict *msg = xs_dict_get(q_item, "message");
 797+        const xs_dict *msg = xs_dict_get(q_item, "message");
 798         xs *rcpts    = recipient_list(snac, msg, 1);
 799         xs_set inboxes;
 800         xs_list *p;
 801-        xs_str *actor;
 802+        const xs_str *actor;
 803 
 804         xs_set_init(&inboxes);
 805 
 806@@ -2237,7 +2212,7 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
 807         if (is_msg_public(msg)) {
 808             if (xs_type(xs_dict_get(srv_config, "disable_inbox_collection")) != XSTYPE_TRUE) {
 809                 xs *shibx = inbox_list();
 810-                xs_str *inbox;
 811+                const xs_str *inbox;
 812 
 813                 p = shibx;
 814                 while (xs_list_iter(&p, &inbox)) {
 815@@ -2252,8 +2227,8 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
 816     else
 817     if (strcmp(type, "input") == 0) {
 818         /* process the message */
 819-        xs_dict *msg = xs_dict_get(q_item, "message");
 820-        xs_dict *req = xs_dict_get(q_item, "req");
 821+        const xs_dict *msg = xs_dict_get(q_item, "message");
 822+        const xs_dict *req = xs_dict_get(q_item, "req");
 823         int retries  = xs_number_get(xs_dict_get(q_item, "retries"));
 824 
 825         if (xs_is_null(msg))
 826@@ -2280,11 +2255,20 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
 827             update_question(snac, id);
 828     }
 829     else
 830-    if (strcmp(type, "request_replies") == 0) {
 831+    if (strcmp(type, "object_request") == 0) {
 832         const char *id = xs_dict_get(q_item, "message");
 833 
 834-        if (!xs_is_null(id))
 835-            timeline_request_replies(snac, id);
 836+        if (!xs_is_null(id)) {
 837+            int status;
 838+            xs *data = NULL;
 839+
 840+            status = activitypub_request(snac, id, &data);
 841+
 842+            if (valid_status(status))
 843+                object_add_ow(id, data);
 844+
 845+            snac_debug(snac, 1, xs_fmt("object_request %s %d", id, status));
 846+        }
 847     }
 848     else
 849     if (strcmp(type, "verify_links") == 0) {
 850@@ -2320,7 +2304,7 @@ int process_user_queue(snac *snac)
 851     xs *list = user_queue(snac);
 852 
 853     xs_list *p = list;
 854-    xs_str *fn;
 855+    const xs_str *fn;
 856 
 857     while (xs_list_iter(&p, &fn)) {
 858         xs *q_item = dequeue(fn);
 859@@ -2339,19 +2323,20 @@ int process_user_queue(snac *snac)
 860 void process_queue_item(xs_dict *q_item)
 861 /* processes an item from the global queue */
 862 {
 863-    char *type = xs_dict_get(q_item, "type");
 864+    const char *type = xs_dict_get(q_item, "type");
 865     int queue_retry_max = xs_number_get(xs_dict_get(srv_config, "queue_retry_max"));
 866 
 867     if (strcmp(type, "output") == 0) {
 868         int status;
 869-        xs_str *inbox  = xs_dict_get(q_item, "inbox");
 870-        xs_str *keyid  = xs_dict_get(q_item, "keyid");
 871-        xs_str *seckey = xs_dict_get(q_item, "seckey");
 872-        xs_dict *msg   = xs_dict_get(q_item, "message");
 873+        const xs_str *inbox  = xs_dict_get(q_item, "inbox");
 874+        const xs_str *keyid  = xs_dict_get(q_item, "keyid");
 875+        const xs_str *seckey = xs_dict_get(q_item, "seckey");
 876+        const xs_dict *msg   = xs_dict_get(q_item, "message");
 877         int retries    = xs_number_get(xs_dict_get(q_item, "retries"));
 878         int p_status   = xs_number_get(xs_dict_get(q_item, "p_status"));
 879         xs *payload    = NULL;
 880         int p_size     = 0;
 881+        int timeout    = 0;
 882 
 883         if (xs_is_null(inbox) || xs_is_null(msg) || xs_is_null(keyid) || xs_is_null(seckey)) {
 884             srv_log(xs_fmt("output message error: missing fields"));
 885@@ -2364,8 +2349,15 @@ void process_queue_item(xs_dict *q_item)
 886         }
 887 
 888         /* deliver (if previous error status was a timeout, try now longer) */
 889-        status = send_to_inbox_raw(keyid, seckey, inbox, msg,
 890-                    &payload, &p_size, p_status == 599 ? 8 : 6);
 891+        if (p_status == 599)
 892+            timeout = xs_number_get(xs_dict_get_def(srv_config, "queue_timeout_2", "8"));
 893+        else
 894+            timeout = xs_number_get(xs_dict_get_def(srv_config, "queue_timeout", "6"));
 895+
 896+        if (timeout == 0)
 897+            timeout = 6;
 898+
 899+        status = send_to_inbox_raw(keyid, seckey, inbox, msg, &payload, &p_size, timeout);
 900 
 901         if (payload) {
 902             if (p_size > 64) {
 903@@ -2411,7 +2403,7 @@ void process_queue_item(xs_dict *q_item)
 904     else
 905     if (strcmp(type, "email") == 0) {
 906         /* send this email */
 907-        xs_str *msg = xs_dict_get(q_item, "message");
 908+        const xs_str *msg = xs_dict_get(q_item, "message");
 909         int retries = xs_number_get(xs_dict_get(q_item, "retries"));
 910 
 911         if (!send_email(msg))
 912@@ -2433,8 +2425,8 @@ void process_queue_item(xs_dict *q_item)
 913     else
 914     if (strcmp(type, "telegram") == 0) {
 915         /* send this via telegram */
 916-        char *bot   = xs_dict_get(q_item, "bot");
 917-        char *msg   = xs_dict_get(q_item, "message");
 918+        const char *bot   = xs_dict_get(q_item, "bot");
 919+        const char *msg   = xs_dict_get(q_item, "message");
 920         xs *chat_id = xs_dup(xs_dict_get(q_item, "chat_id"));
 921         int status  = 0;
 922 
 923@@ -2457,9 +2449,9 @@ void process_queue_item(xs_dict *q_item)
 924     else
 925     if (strcmp(type, "ntfy") == 0) {
 926         /* send this via ntfy */
 927-        char *ntfy_server   = xs_dict_get(q_item, "ntfy_server");
 928-        char *msg   = xs_dict_get(q_item, "message");
 929-        char *ntfy_token   = xs_dict_get(q_item, "ntfy_token");
 930+        const char *ntfy_server   = xs_dict_get(q_item, "ntfy_server");
 931+        const char *msg   = xs_dict_get(q_item, "message");
 932+        const char *ntfy_token   = xs_dict_get(q_item, "ntfy_token");
 933         int status  = 0;
 934 
 935         xs *url  = xs_fmt("%s", ntfy_server);
 936@@ -2488,8 +2480,8 @@ void process_queue_item(xs_dict *q_item)
 937     }
 938     else
 939     if (strcmp(type, "input") == 0) {
 940-        xs_dict *msg = xs_dict_get(q_item, "message");
 941-        xs_dict *req = xs_dict_get(q_item, "req");
 942+        const xs_dict *msg = xs_dict_get(q_item, "message");
 943+        const xs_dict *req = xs_dict_get(q_item, "req");
 944         int retries  = xs_number_get(xs_dict_get(q_item, "retries"));
 945 
 946         /* do some instance-level checks */
 947@@ -2497,8 +2489,6 @@ void process_queue_item(xs_dict *q_item)
 948 
 949         if (r == 0) {
 950             /* transient error? retry */
 951-            int queue_retry_max = xs_number_get(xs_dict_get(srv_config, "queue_retry_max"));
 952-
 953             if (retries > queue_retry_max)
 954                 srv_log(xs_fmt("shared input giving up"));
 955             else {
 956@@ -2510,7 +2500,7 @@ void process_queue_item(xs_dict *q_item)
 957         else
 958         if (r == 2) {
 959             /* redistribute the input message to all users */
 960-            char *ntid = xs_dict_get(q_item, "ntid");
 961+            const char *ntid = xs_dict_get(q_item, "ntid");
 962             xs *tmpfn  = xs_fmt("%s/tmp/%s.json", srv_basedir, ntid);
 963             FILE *f;
 964 
 965@@ -2521,7 +2511,7 @@ void process_queue_item(xs_dict *q_item)
 966 
 967             xs *users = user_list();
 968             xs_list *p = users;
 969-            char *v;
 970+            const char *v;
 971             int cnt = 0;
 972 
 973             while (xs_list_iter(&p, &v)) {
 974@@ -2564,7 +2554,7 @@ int process_queue(void)
 975     xs *list = queue();
 976 
 977     xs_list *p = list;
 978-    xs_str *fn;
 979+    const xs_str *fn;
 980 
 981     while (xs_list_iter(&p, &fn)) {
 982         xs *q_item = dequeue(fn);
 983@@ -2585,7 +2575,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
 984                             char **body, int *b_size, char **ctype)
 985 {
 986     int status = 200;
 987-    char *accept = xs_dict_get(req, "accept");
 988+    const char *accept = xs_dict_get(req, "accept");
 989     snac snac;
 990     xs *msg = NULL;
 991 
 992@@ -2597,7 +2587,8 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
 993         return 0;
 994 
 995     xs *l = xs_split_n(q_path, "/", 2);
 996-    char *uid, *p_path;
 997+    const char *uid;
 998+    const char *p_path;
 999 
1000     uid = xs_list_get(l, 1);
1001     if (!user_open(&snac, uid)) {
1002@@ -2615,7 +2606,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
1003         msg = msg_actor(&snac);
1004         *ctype = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
1005 
1006-        char *ua = xs_dict_get(req, "user-agent");
1007+        const char *ua = xs_dict_get(req, "user-agent");
1008 
1009         snac_debug(&snac, 0, xs_fmt("serving actor [%s]", ua ? ua : "No UA"));
1010     }
1011@@ -2625,15 +2616,16 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
1012         xs *elems = timeline_simple_list(&snac, "public", 0, 20);
1013         xs *list = xs_list_new();
1014         msg = msg_collection(&snac, id);
1015-        char *p, *v;
1016+        char *p;
1017+        const char *v;
1018 
1019         p = elems;
1020         while (xs_list_iter(&p, &v)) {
1021             xs *i = NULL;
1022 
1023             if (valid_status(object_get_by_md5(v, &i))) {
1024-                char *type = xs_dict_get(i, "type");
1025-                char *id   = xs_dict_get(i, "id");
1026+                const char *type = xs_dict_get(i, "type");
1027+                const char *id   = xs_dict_get(i, "id");
1028 
1029                 if (type && id && strcmp(type, "Note") == 0 && xs_startswith(id, snac.actor)) {
1030                     xs *c_msg = msg_create(&snac, i);
1031@@ -2686,9 +2678,9 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
1032     (void)b_size;
1033 
1034     int status = 202; /* accepted */
1035-    char *i_ctype = xs_dict_get(req, "content-type");
1036+    const char *i_ctype = xs_dict_get(req, "content-type");
1037     snac snac;
1038-    char *v;
1039+    const char *v;
1040 
1041     if (i_ctype == NULL) {
1042         *body  = xs_str_new("no content-type");
M data.c
+465, -121
   1@@ -10,6 +10,7 @@
   2 #include "xs_set.h"
   3 #include "xs_time.h"
   4 #include "xs_regex.h"
   5+#include "xs_match.h"
   6 
   7 #include "snac.h"
   8 
   9@@ -28,7 +29,7 @@ pthread_mutex_t data_mutex = {0};
  10 int snac_upgrade(xs_str **error);
  11 
  12 
  13-int srv_open(char *basedir, int auto_upgrade)
  14+int srv_open(const char *basedir, int auto_upgrade)
  15 /* opens a server */
  16 {
  17     int ret = 0;
  18@@ -57,18 +58,20 @@ int srv_open(char *basedir, int auto_upgrade)
  19         if (srv_config == NULL)
  20             error = xs_fmt("ERROR: cannot parse '%s'", cfg_file);
  21         else {
  22-            char *host;
  23-            char *prefix;
  24-            char *dbglvl;
  25+            const char *host;
  26+            const char *prefix;
  27+            const char *dbglvl;
  28+            const char *proto;
  29 
  30             host   = xs_dict_get(srv_config, "host");
  31             prefix = xs_dict_get(srv_config, "prefix");
  32             dbglvl = xs_dict_get(srv_config, "dbglevel");
  33+            proto  = xs_dict_get_def(srv_config, "protocol", "https");
  34 
  35             if (host == NULL || prefix == NULL)
  36                 error = xs_str_new("ERROR: cannot get server data");
  37             else {
  38-                srv_baseurl = xs_fmt("https://%s%s", host, prefix);
  39+                srv_baseurl = xs_fmt("%s:/" "/%s%s", proto, host, prefix);
  40 
  41                 dbglevel = (int) xs_number_get(dbglvl);
  42 
  43@@ -111,7 +114,7 @@ int srv_open(char *basedir, int auto_upgrade)
  44 #endif
  45 
  46 #ifdef __OpenBSD__
  47-    char *v = xs_dict_get(srv_config, "disable_openbsd_security");
  48+    const char *v = xs_dict_get(srv_config, "disable_openbsd_security");
  49 
  50     if (v && xs_type(v) == XSTYPE_TRUE) {
  51         srv_debug(1, xs_dup("OpenBSD security disabled by admin"));
  52@@ -190,7 +193,7 @@ int user_open(snac *user, const char *uid)
  53             xs *lcuid = xs_tolower_i(xs_dup(uid));
  54             xs *ulist = user_list();
  55             xs_list *p = ulist;
  56-            xs_str *v;
  57+            const xs_str *v;
  58 
  59             while (xs_list_iter(&p, &v)) {
  60                 xs *v2 = xs_tolower_i(xs_dup(v));
  61@@ -286,7 +289,7 @@ int user_open_by_md5(snac *snac, const char *md5)
  62 {
  63     xs *ulist  = user_list();
  64     xs_list *p = ulist;
  65-    xs_str *v;
  66+    const xs_str *v;
  67 
  68     while (xs_list_iter(&p, &v)) {
  69         user_open(snac, v);
  70@@ -338,6 +341,12 @@ double f_ctime(const char *fn)
  71 }
  72 
  73 
  74+int is_md5_hex(const char *md5)
  75+{
  76+    return xs_is_hex(md5) && strlen(md5) == 32;
  77+}
  78+
  79+
  80 /** database 2.1+ **/
  81 
  82 /** indexes **/
  83@@ -349,6 +358,11 @@ int index_add_md5(const char *fn, const char *md5)
  84     int status = 201; /* Created */
  85     FILE *f;
  86 
  87+    if (!is_md5_hex(md5)) {
  88+        srv_log(xs_fmt("index_add_md5: bad md5 %s %s", fn, md5));
  89+        return 400;
  90+    }
  91+
  92     pthread_mutex_lock(&data_mutex);
  93 
  94     if ((f = fopen(fn, "a")) != NULL) {
  95@@ -406,7 +420,7 @@ int index_del_md5(const char *fn, const char *md5)
  96         fclose(f);
  97     }
  98     else
  99-        status = 500;
 100+        status = 410;
 101 
 102     pthread_mutex_unlock(&data_mutex);
 103 
 104@@ -604,7 +618,7 @@ static xs_str *_object_fn_by_md5(const char *md5, const char *func)
 105     if (md5[0] == '-')
 106         ok = 0;
 107     else
 108-    if (!xs_is_hex(md5) || strlen(md5) != 32) {
 109+    if (!is_md5_hex(md5)) {
 110         srv_log(xs_fmt("_object_fn_by_md5() [from %s()]: bad md5 '%s'", func, md5));
 111         ok = 0;
 112     }
 113@@ -696,7 +710,7 @@ int _object_add(const char *id, const xs_dict *obj, int ow)
 114         fclose(f);
 115 
 116         /* does this object has a parent? */
 117-        char *in_reply_to = xs_dict_get(obj, "inReplyTo");
 118+        const char *in_reply_to = xs_dict_get(obj, "inReplyTo");
 119 
 120         if (!xs_is_null(in_reply_to) && *in_reply_to) {
 121             /* update the children index of the parent */
 122@@ -758,7 +772,8 @@ int object_del_by_md5(const char *md5)
 123         xs *spec  = xs_dup(fn);
 124         spec      = xs_replace_i(spec, ".json", "*.idx");
 125         xs *files = xs_glob(spec, 0, 0);
 126-        char *p, *v;
 127+        char *p;
 128+        const char *v;
 129 
 130         p = files;
 131         while (xs_list_iter(&p, &v)) {
 132@@ -917,6 +932,9 @@ int object_unadmire(const char *id, const char *actor, int like)
 133 
 134     status = index_del(fn, actor);
 135 
 136+    if (valid_status(status))
 137+        index_gc(fn);
 138+
 139     srv_debug(0,
 140         xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
 141 
 142@@ -1016,7 +1034,8 @@ xs_list *follower_list(snac *snac)
 143 {
 144     xs *list       = object_user_cache_list(snac, "followers", XS_ALL, 0);
 145     xs_list *fwers = xs_list_new();
 146-    char *p, *v;
 147+    char *p;
 148+    const char *v;
 149 
 150     /* resolve the list of md5 to be a list of actors */
 151     p = list;
 152@@ -1060,14 +1079,18 @@ int timeline_touch(snac *snac)
 153 xs_str *timeline_fn_by_md5(snac *snac, const char *md5)
 154 /* get the filename of an entry by md5 from any timeline */
 155 {
 156-    xs_str *fn = xs_fmt("%s/private/%s.json", snac->basedir, md5);
 157+    xs_str *fn = NULL;
 158 
 159-    if (mtime(fn) == 0.0) {
 160-        fn = xs_free(fn);
 161-        fn = xs_fmt("%s/public/%s.json", snac->basedir, md5);
 162+    if (xs_is_hex(md5) && strlen(md5) == 32) {
 163+        fn = xs_fmt("%s/private/%s.json", snac->basedir, md5);
 164 
 165-        if (mtime(fn) == 0.0)
 166+        if (mtime(fn) == 0.0) {
 167             fn = xs_free(fn);
 168+            fn = xs_fmt("%s/public/%s.json", snac->basedir, md5);
 169+
 170+            if (mtime(fn) == 0.0)
 171+                fn = xs_free(fn);
 172+        }
 173     }
 174 
 175     return fn;
 176@@ -1103,7 +1126,7 @@ int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
 177 }
 178 
 179 
 180-int timeline_del(snac *snac, char *id)
 181+int timeline_del(snac *snac, const char *id)
 182 /* deletes a message from the timeline */
 183 {
 184     /* delete from the user's caches */
 185@@ -1145,6 +1168,8 @@ int timeline_add(snac *snac, const char *id, const xs_dict *o_msg)
 186 
 187     tag_index(id, o_msg);
 188 
 189+    list_distribute(snac, NULL, o_msg);
 190+
 191     snac_debug(snac, 1, xs_fmt("timeline_add %s", id));
 192 
 193     return ret;
 194@@ -1169,17 +1194,16 @@ int timeline_admire(snac *snac, const char *id, const char *admirer, int like)
 195 }
 196 
 197 
 198-xs_list *timeline_top_level(snac *snac, xs_list *list)
 199+xs_list *timeline_top_level(snac *snac, const xs_list *list)
 200 /* returns the top level md5 entries from this index */
 201 {
 202     xs_set seen;
 203-    xs_list *p;
 204-    xs_str *v;
 205+    const xs_str *v;
 206 
 207     xs_set_init(&seen);
 208 
 209-    p = list;
 210-    while (xs_list_iter(&p, &v)) {
 211+    int c = 0;
 212+    while (xs_list_next(list, &v, &c)) {
 213         char line[256] = "";
 214 
 215         strncpy(line, v, sizeof(line));
 216@@ -1267,7 +1291,7 @@ int following_add(snac *snac, const char *actor, const xs_dict *msg)
 217         /* object already exists; if it's of type Accept,
 218            the actor is already being followed and confirmed,
 219            so do nothing */
 220-        char *type = xs_dict_get(p_object, "type");
 221+        const char *type = xs_dict_get(p_object, "type");
 222 
 223         if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
 224             snac_debug(snac, 1, xs_fmt("following_add actor already confirmed %s", actor));
 225@@ -1345,7 +1369,7 @@ xs_list *following_list(snac *snac)
 226     xs *spec = xs_fmt("%s/following/" "*.json", snac->basedir);
 227     xs *glist = xs_glob(spec, 0, 0);
 228     xs_list *p;
 229-    xs_str *v;
 230+    const xs_str *v;
 231     xs_list *list = xs_list_new();
 232 
 233     /* iterate the list of files */
 234@@ -1515,7 +1539,8 @@ void hide(snac *snac, const char *id)
 235 
 236         /* hide all the children */
 237         xs *chld = object_children(id);
 238-        char *p, *v;
 239+        char *p;
 240+        const char *v;
 241 
 242         p = chld;
 243         while (xs_list_iter(&p, &v)) {
 244@@ -1523,8 +1548,9 @@ void hide(snac *snac, const char *id)
 245 
 246             /* resolve to get the id */
 247             if (valid_status(object_get_by_md5(v, &co))) {
 248-                if ((v = xs_dict_get(co, "id")) != NULL)
 249-                    hide(snac, v);
 250+                const char *id = xs_dict_get(co, "id");
 251+                if (id != NULL)
 252+                    hide(snac, id);
 253             }
 254         }
 255     }
 256@@ -1540,7 +1566,7 @@ int is_hidden(snac *snac, const char *id)
 257 }
 258 
 259 
 260-int actor_add(const char *actor, xs_dict *msg)
 261+int actor_add(const char *actor, const xs_dict *msg)
 262 /* adds an actor */
 263 {
 264     return object_add_ow(actor, msg);
 265@@ -1609,7 +1635,7 @@ int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
 266     int status = actor_get(actor, data);
 267 
 268     if (status == 205 && user && !xs_startswith(actor, srv_baseurl))
 269-        enqueue_actor_refresh(user, actor);
 270+        enqueue_actor_refresh(user, actor, 0);
 271 
 272     return status;
 273 }
 274@@ -1664,17 +1690,18 @@ int limited(snac *user, const char *id, int cmd)
 275 void tag_index(const char *id, const xs_dict *obj)
 276 /* update the tag indexes for this object */
 277 {
 278-    xs_list *tags = xs_dict_get(obj, "tag");
 279+    const xs_list *tags = xs_dict_get(obj, "tag");
 280 
 281     if (is_msg_public(obj) && xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
 282         xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
 283 
 284         mkdirx(g_tag_dir);
 285 
 286-        xs_dict *v;
 287-        while (xs_list_iter(&tags, &v)) {
 288-            char *type = xs_dict_get(v, "type");
 289-            char *name = xs_dict_get(v, "name");
 290+        const xs_dict *v;
 291+        int ct = 0;
 292+        while (xs_list_next(tags, &v, &ct)) {
 293+            const char *type = xs_dict_get(v, "type");
 294+            const char *name = xs_dict_get(v, "name");
 295 
 296             if (!xs_is_null(type) && !xs_is_null(name) && strcmp(type, "Hashtag") == 0) {
 297                 while (*name == '#' || *name == '@')
 298@@ -1683,7 +1710,7 @@ void tag_index(const char *id, const xs_dict *obj)
 299                 if (*name == '\0')
 300                     continue;
 301 
 302-                name = xs_tolower_i(name);
 303+                name = xs_tolower_i((xs_str *)name);
 304 
 305                 xs *md5_tag   = xs_md5_hex(name, strlen(name));
 306                 xs *tag_dir   = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]);
 307@@ -1706,7 +1733,7 @@ void tag_index(const char *id, const xs_dict *obj)
 308 }
 309 
 310 
 311-xs_list *tag_search(char *tag, int skip, int show)
 312+xs_list *tag_search(const char *tag, int skip, int show)
 313 /* returns the list of posts tagged with tag */
 314 {
 315     if (*tag == '#')
 316@@ -1720,6 +1747,206 @@ xs_list *tag_search(char *tag, int skip, int show)
 317 }
 318 
 319 
 320+/** lists **/
 321+
 322+xs_val *list_maint(snac *user, const char *list, int op)
 323+/* list maintenance */
 324+{
 325+    xs_val *l = NULL;
 326+
 327+    switch (op) {
 328+    case 0: /** list of lists **/
 329+        {
 330+            FILE *f;
 331+            xs *spec = xs_fmt("%s/list/" "*.id", user->basedir);
 332+            xs *ls   = xs_glob(spec, 0, 0);
 333+            int c = 0;
 334+            const char *v;
 335+
 336+            l = xs_list_new();
 337+
 338+            while (xs_list_next(ls, &v, &c)) {
 339+                if ((f = fopen(v, "r")) != NULL) {
 340+                    xs *title = xs_readline(f);
 341+                    fclose(f);
 342+
 343+                    title = xs_strip_i(title);
 344+
 345+                    xs *v2 = xs_replace(v, ".id", "");
 346+                    xs *l2 = xs_split(v2, "/");
 347+
 348+                    /* return [ list_id, list_title ] */
 349+                    l = xs_list_append(l, xs_list_append(xs_list_new(), xs_list_get(l2, -1), title));
 350+                }
 351+            }
 352+        }
 353+
 354+        break;
 355+
 356+    case 1: /** create new list (list is the name) **/
 357+        {
 358+            xs *lol = list_maint(user, NULL, 0);
 359+            int c = 0;
 360+            const xs_list *v;
 361+            int add = 1;
 362+
 363+            /* check if this list name already exists */
 364+            while (xs_list_next(lol, &v, &c)) {
 365+                if (strcmp(xs_list_get(v, 1), list) == 0) {
 366+                    add = 0;
 367+                    break;
 368+                }
 369+            }
 370+
 371+            if (add) {
 372+                FILE *f;
 373+                xs *dir = xs_fmt("%s/list/", user->basedir);
 374+                xs *id  = xs_fmt("%010x", time(NULL));
 375+
 376+                mkdirx(dir);
 377+
 378+                xs *fn = xs_fmt("%s%s.id", dir, id);
 379+
 380+                if ((f = fopen(fn, "w")) != NULL) {
 381+                    fprintf(f, "%s\n", list);
 382+                    fclose(f);
 383+                }
 384+
 385+                l = xs_stock(XSTYPE_TRUE);
 386+            }
 387+            else
 388+                l = xs_stock(XSTYPE_FALSE);
 389+        }
 390+
 391+        break;
 392+
 393+    case 2: /** delete list (list is the id) **/
 394+        {
 395+            if (xs_is_hex(list)) {
 396+                xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
 397+                unlink(fn);
 398+
 399+                fn = xs_replace_i(fn, ".id", ".lst");
 400+                unlink(fn);
 401+
 402+                fn = xs_replace_i(fn, ".lst", ".idx");
 403+                unlink(fn);
 404+
 405+                fn = xs_str_cat(fn, ".bak");
 406+                unlink(fn);
 407+            }
 408+        }
 409+
 410+        break;
 411+
 412+    case 3: /** get list name **/
 413+        if (xs_is_hex(list)) {
 414+            FILE *f;
 415+            xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
 416+
 417+            if ((f = fopen(fn, "r")) != NULL) {
 418+                l = xs_strip_i(xs_readline(f));
 419+                fclose(f);
 420+            }
 421+        }
 422+
 423+        break;
 424+    }
 425+
 426+    return l;
 427+}
 428+
 429+
 430+xs_list *list_timeline(snac *user, const char *list, int skip, int show)
 431+/* returns the timeline of a list */
 432+{
 433+    xs_list *l = NULL;
 434+
 435+    if (!xs_is_hex(list))
 436+        return NULL;
 437+
 438+    xs *fn = xs_fmt("%s/list/%s.idx", user->basedir, list);
 439+
 440+    if (mtime(fn) > 0.0)
 441+        l = index_list_desc(fn, skip, show);
 442+
 443+    return l;
 444+}
 445+
 446+
 447+xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op)
 448+/* list content management */
 449+{
 450+    xs_val *l = NULL;
 451+
 452+    if (!xs_is_hex(list))
 453+        return NULL;
 454+
 455+    if (actor_md5 != NULL && !xs_is_hex(actor_md5))
 456+        return NULL;
 457+
 458+    xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
 459+
 460+    switch (op) {
 461+    case 0: /** list content **/
 462+        l = index_list(fn, XS_ALL);
 463+
 464+        break;
 465+
 466+    case 1: /** append actor to list **/
 467+        if (actor_md5 != NULL) {
 468+            if (!index_in(fn, actor_md5))
 469+                index_add_md5(fn, actor_md5);
 470+        }
 471+
 472+        break;
 473+
 474+    case 2: /** delete actor from list **/
 475+        if (actor_md5 != NULL)
 476+            index_del_md5(fn, actor_md5);
 477+
 478+        break;
 479+
 480+    default:
 481+        srv_log(xs_fmt("ERROR: list_content: bad op %d", op));
 482+        break;
 483+    }
 484+
 485+    return l;
 486+}
 487+
 488+
 489+void list_distribute(snac *user, const char *who, const xs_dict *post)
 490+/* distributes the post to all appropriate lists */
 491+{
 492+    const char *id = xs_dict_get(post, "id");
 493+
 494+    /* if who is not set, use the attributedTo in the message */
 495+    if (xs_is_null(who))
 496+        who = get_atto(post);
 497+
 498+    if (xs_type(who) == XSTYPE_STRING && xs_type(id) == XSTYPE_STRING) {
 499+        xs *a_md5 = xs_md5_hex(who, strlen(who));
 500+        xs *i_md5 = xs_md5_hex(id, strlen(id));
 501+        xs *spec  = xs_fmt("%s/list/" "*.lst", user->basedir);
 502+        xs *ls    = xs_glob(spec, 0, 0);
 503+        int c = 0;
 504+        const char *v;
 505+
 506+        while (xs_list_next(ls, &v, &c)) {
 507+            /* is the actor in this list? */
 508+            if (index_in_md5(v, a_md5)) {
 509+                /* it is; add post md5 to its timeline */
 510+                xs *idx = xs_replace(v, ".lst", ".idx");
 511+                index_add_md5(idx, i_md5);
 512+
 513+                snac_debug(user, 1, xs_fmt("listed post %s in %s", id, idx));
 514+            }
 515+        }
 516+    }
 517+}
 518+
 519+
 520 /** static data **/
 521 
 522 static int _load_raw_file(const char *fn, xs_val **data, int *size,
 523@@ -1944,7 +2171,7 @@ void inbox_add(const char *inbox)
 524 void inbox_add_by_actor(const xs_dict *actor)
 525 /* collects an actor's shared inbox, if it has one */
 526 {
 527-    char *v;
 528+    const char *v;
 529 
 530     if (!xs_is_null(v = xs_dict_get(actor, "endpoints")) &&
 531         !xs_is_null(v = xs_dict_get(v, "sharedInbox"))) {
 532@@ -1962,7 +2189,7 @@ xs_list *inbox_list(void)
 533     xs *spec     = xs_fmt("%s/inbox/" "*", srv_basedir);
 534     xs *files    = xs_glob(spec, 0, 0);
 535     xs_list *p   = files;
 536-    xs_val *v;
 537+    const xs_val *v;
 538 
 539     while (xs_list_iter(&p, &v)) {
 540         FILE *f;
 541@@ -1987,9 +2214,10 @@ xs_list *inbox_list(void)
 542 
 543 xs_str *_instance_block_fn(const char *instance)
 544 {
 545-    xs *s1  = xs_replace(instance, "https:/" "/", "");
 546+    xs *s   = xs_replace(instance, "http:/" "/", "");
 547+    xs *s1  = xs_replace(s, "https:/" "/", "");
 548     xs *l   = xs_split(s1, "/");
 549-    char *p = xs_list_get(l, 0);
 550+    const char *p = xs_list_get(l, 0);
 551     xs *md5 = xs_md5_hex(p, strlen(p));
 552 
 553     return xs_fmt("%s/block/%s", srv_basedir, md5);
 554@@ -2049,20 +2277,20 @@ int instance_unblock(const char *instance)
 555 }
 556 
 557 
 558-/** content filtering **/
 559+/** operations by content **/
 560 
 561-int content_check(const char *file, const xs_dict *msg)
 562+int content_match(const char *file, const xs_dict *msg)
 563 /* checks if a message's content matches any of the regexes in file */
 564 /* file format: one regex per line */
 565 {
 566     xs *fn = xs_fmt("%s/%s", srv_basedir, file);
 567     FILE *f;
 568     int r = 0;
 569-    char *v = xs_dict_get(msg, "content");
 570+    const char *v = xs_dict_get(msg, "content");
 571 
 572     if (xs_type(v) == XSTYPE_STRING && *v) {
 573         if ((f = fopen(fn, "r")) != NULL) {
 574-            srv_debug(1, xs_fmt("content_check: loading regexes from %s", fn));
 575+            srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn));
 576 
 577             /* massage content (strip HTML tags, etc.) */
 578             xs *c = xs_regex_replace(v, "<[^>]+>", " ");
 579@@ -2072,13 +2300,9 @@ int content_check(const char *file, const xs_dict *msg)
 580             while (!r && !feof(f)) {
 581                 xs *rx = xs_strip_i(xs_readline(f));
 582 
 583-                if (*rx) {
 584-                    xs *l = xs_regex_select_n(c, rx, 1);
 585-
 586-                    if (xs_list_len(l)) {
 587-                        srv_debug(1, xs_fmt("content_check: match for '%s'", rx));
 588-                        r = 1;
 589-                    }
 590+                if (*rx && xs_regex_match(c, rx)) {
 591+                    srv_debug(1, xs_fmt("content_match: match for '%s'", rx));
 592+                    r = 1;
 593                 }
 594             }
 595 
 596@@ -2090,6 +2314,119 @@ int content_check(const char *file, const xs_dict *msg)
 597 }
 598 
 599 
 600+xs_list *content_search(snac *user, const char *regex,
 601+                int priv, int skip, int show, int max_secs, int *timeout)
 602+/* returns a list of posts which content matches the regex */
 603+{
 604+    if (regex == NULL || *regex == '\0')
 605+        return xs_list_new();
 606+
 607+    xs *i_regex = xs_tolower_i(xs_dup(regex));
 608+
 609+    xs_set seen;
 610+
 611+    xs_set_init(&seen);
 612+
 613+    if (max_secs == 0)
 614+        max_secs = 3;
 615+
 616+    time_t t = time(NULL) + max_secs;
 617+    *timeout = 0;
 618+
 619+    /* iterate all timelines simultaneously */
 620+    xs_list *tls[3] = {0};
 621+    const char *md5s[3] = {0};
 622+    int c[3] = {0};
 623+
 624+    tls[0] = timeline_simple_list(user, "public", 0, XS_ALL);   /* public */
 625+    tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
 626+    tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL) : xs_list_new(); /* private or none */
 627+
 628+    /* first positioning */
 629+    for (int n = 0; n < 3; n++)
 630+        xs_list_next(tls[n], &md5s[n], &c[n]);
 631+
 632+    show += skip;
 633+
 634+    while (show > 0) {
 635+        /* timeout? */
 636+        if (time(NULL) > t) {
 637+            *timeout = 1;
 638+            break;
 639+        }
 640+
 641+        /* find the newest post */
 642+        int newest = -1;
 643+        double mtime = 0.0;
 644+
 645+        for (int n = 0; n < 3; n++) {
 646+            if (md5s[n] != NULL) {
 647+                xs *fn = _object_fn_by_md5(md5s[n], "content_search");
 648+                double mt = mtime(fn);
 649+
 650+                if (mt > mtime) {
 651+                    newest = n;
 652+                    mtime = mt;
 653+                }
 654+            }
 655+        }
 656+
 657+        if (newest == -1)
 658+            break;
 659+
 660+        const char *md5 = md5s[newest];
 661+
 662+        /* advance the chosen timeline */
 663+        if (!xs_list_next(tls[newest], &md5s[newest], &c[newest]))
 664+            md5s[newest] = NULL;
 665+
 666+        xs *post = NULL;
 667+
 668+        if (!valid_status(object_get_by_md5(md5, &post)))
 669+            continue;
 670+
 671+        if (!xs_match(xs_dict_get_def(post, "type", "-"), POSTLIKE_OBJECT_TYPE))
 672+            continue;
 673+
 674+        const char *id = xs_dict_get(post, "id");
 675+
 676+        if (id == NULL || is_hidden(user, id))
 677+            continue;
 678+
 679+        const char *content = xs_dict_get(post, "content");
 680+
 681+        if (xs_is_null(content))
 682+            continue;
 683+
 684+        /* strip HTML */
 685+        xs *c = xs_regex_replace(content, "<[^>]+>", " ");
 686+        c = xs_regex_replace_i(c, " {2,}", " ");
 687+        c = xs_tolower_i(c);
 688+
 689+        /* apply regex */
 690+        if (xs_regex_match(c, i_regex)) {
 691+            if (xs_set_add(&seen, md5) == 1)
 692+            show--;
 693+        }
 694+    }
 695+
 696+    xs_list *r = xs_set_result(&seen);
 697+
 698+    if (skip) {
 699+        /* BAD */
 700+        while (skip--) {
 701+            r = xs_list_del(r, 0);
 702+        }
 703+    }
 704+
 705+    xs_free(tls[0]);
 706+    xs_free(tls[1]);
 707+    xs_free(tls[2]);
 708+
 709+    return r;
 710+}
 711+
 712+
 713 /** notifications **/
 714 
 715 xs_str *notify_check_time(snac *snac, int reset)
 716@@ -2203,7 +2540,7 @@ xs_list *notify_list(snac *snac, int skip, int show)
 717             xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
 718             xs *lst  = xs_glob(spec, 1, 0);
 719             xs_list *p = lst;
 720-            char *v;
 721+            const char *v;
 722 
 723             while (xs_list_iter(&p, &v)) {
 724                 char *p = strrchr(v, '.');
 725@@ -2231,7 +2568,7 @@ int notify_new_num(snac *snac)
 726     int cnt = 0;
 727 
 728     xs_list *p = lst;
 729-    xs_str *v;
 730+    const xs_str *v;
 731 
 732     while (xs_list_iter(&p, &v)) {
 733         xs *id = xs_strip_i(xs_dup(v));
 734@@ -2253,7 +2590,7 @@ void notify_clear(snac *snac)
 735     xs *spec   = xs_fmt("%s/notify/" "*", snac->basedir);
 736     xs *lst    = xs_glob(spec, 0, 0);
 737     xs_list *p = lst;
 738-    xs_str *v;
 739+    const xs_str *v;
 740 
 741     while (xs_list_iter(&p, &v))
 742         unlink(v);
 743@@ -2309,7 +2646,7 @@ void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retri
 744 /* enqueues an input message */
 745 {
 746     xs *qmsg   = _new_qmsg("input", msg, retries);
 747-    char *ntid = xs_dict_get(qmsg, "ntid");
 748+    const char *ntid = xs_dict_get(qmsg, "ntid");
 749     xs *fn     = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
 750 
 751     qmsg = xs_dict_append(qmsg, "req", req);
 752@@ -2324,7 +2661,7 @@ void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
 753 /* enqueues an input message from the shared input */
 754 {
 755     xs *qmsg   = _new_qmsg("input", msg, retries);
 756-    char *ntid = xs_dict_get(qmsg, "ntid");
 757+    const char *ntid = xs_dict_get(qmsg, "ntid");
 758     xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
 759 
 760     qmsg = xs_dict_append(qmsg, "req", req);
 761@@ -2336,11 +2673,12 @@ void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
 762 
 763 
 764 void enqueue_output_raw(const char *keyid, const char *seckey,
 765-                        xs_dict *msg, xs_str *inbox, int retries, int p_status)
 766+                        const xs_dict *msg, const xs_str *inbox,
 767+                        int retries, int p_status)
 768 /* enqueues an output message to an inbox */
 769 {
 770     xs *qmsg   = _new_qmsg("output", msg, retries);
 771-    char *ntid = xs_dict_get(qmsg, "ntid");
 772+    const char *ntid = xs_dict_get(qmsg, "ntid");
 773     xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
 774 
 775     xs *ns = xs_number_new(p_status);
 776@@ -2360,7 +2698,8 @@ void enqueue_output_raw(const char *keyid, const char *seckey,
 777 }
 778 
 779 
 780-void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries, int p_status)
 781+void enqueue_output(snac *snac, const xs_dict *msg,
 782+                    const xs_str *inbox, int retries, int p_status)
 783 /* enqueues an output message to an inbox */
 784 {
 785     if (xs_startswith(inbox, snac->actor)) {
 786@@ -2368,13 +2707,14 @@ void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries, int p_
 787         return;
 788     }
 789 
 790-    char *seckey = xs_dict_get(snac->key, "secret");
 791+    const char *seckey = xs_dict_get(snac->key, "secret");
 792 
 793     enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
 794 }
 795 
 796 
 797-void enqueue_output_by_actor(snac *snac, xs_dict *msg, const xs_str *actor, int retries)
 798+void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
 799+                            const xs_str *actor, int retries)
 800 /* enqueues an output message for an actor */
 801 {
 802     xs *inbox = get_actor_inbox(actor);
 803@@ -2386,11 +2726,11 @@ void enqueue_output_by_actor(snac *snac, xs_dict *msg, const xs_str *actor, int
 804 }
 805 
 806 
 807-void enqueue_email(xs_str *msg, int retries)
 808+void enqueue_email(const xs_str *msg, int retries)
 809 /* enqueues an email message to be sent */
 810 {
 811     xs *qmsg   = _new_qmsg("email", msg, retries);
 812-    char *ntid = xs_dict_get(qmsg, "ntid");
 813+    const char *ntid = xs_dict_get(qmsg, "ntid");
 814     xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
 815 
 816     qmsg = _enqueue_put(fn, qmsg);
 817@@ -2403,7 +2743,7 @@ void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
 818 /* enqueues a message to be sent via Telegram */
 819 {
 820     xs *qmsg   = _new_qmsg("telegram", msg, 0);
 821-    char *ntid = xs_dict_get(qmsg, "ntid");
 822+    const char *ntid = xs_dict_get(qmsg, "ntid");
 823     xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
 824 
 825     qmsg = xs_dict_append(qmsg, "bot",      bot);
 826@@ -2418,7 +2758,7 @@ void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_t
 827 /* enqueues a message to be sent via ntfy */
 828 {
 829     xs *qmsg   = _new_qmsg("ntfy", msg, 0);
 830-    char *ntid = xs_dict_get(qmsg, "ntid");
 831+    const char *ntid = xs_dict_get(qmsg, "ntid");
 832     xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
 833 
 834     qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
 835@@ -2434,7 +2774,7 @@ void enqueue_message(snac *snac, const xs_dict *msg)
 836 /* enqueues an output message */
 837 {
 838     xs *qmsg   = _new_qmsg("message", msg, 0);
 839-    char *ntid = xs_dict_get(qmsg, "ntid");
 840+    const char *ntid = xs_dict_get(qmsg, "ntid");
 841     xs *fn     = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
 842 
 843     qmsg = _enqueue_put(fn, qmsg);
 844@@ -2458,67 +2798,47 @@ void enqueue_close_question(snac *user, const char *id, int end_secs)
 845 }
 846 
 847 
 848-void enqueue_verify_links(snac *user)
 849-/* enqueues a link verification */
 850+void enqueue_object_request(snac *user, const char *id, int forward_secs)
 851+/* enqueues the request of an object in the future */
 852 {
 853-    xs *qmsg   = _new_qmsg("verify_links", "", 0);
 854-    char *ntid = xs_dict_get(qmsg, "ntid");
 855-    xs *fn     = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
 856+    xs *qmsg = _new_qmsg("object_request", id, 0);
 857+    xs *ntid = tid(forward_secs);
 858+    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
 859+
 860+    qmsg = xs_dict_set(qmsg, "ntid", ntid);
 861 
 862     qmsg = _enqueue_put(fn, qmsg);
 863 
 864-    snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
 865+    snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
 866 }
 867 
 868 
 869-void enqueue_actor_refresh(snac *user, const char *actor)
 870-/* enqueues an actor refresh */
 871+void enqueue_verify_links(snac *user)
 872+/* enqueues a link verification */
 873 {
 874-    xs *qmsg   = _new_qmsg("actor_refresh", "", 0);
 875-    char *ntid = xs_dict_get(qmsg, "ntid");
 876+    xs *qmsg   = _new_qmsg("verify_links", "", 0);
 877+    const char *ntid = xs_dict_get(qmsg, "ntid");
 878     xs *fn     = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
 879 
 880-    qmsg = xs_dict_append(qmsg, "actor", actor);
 881-
 882     qmsg = _enqueue_put(fn, qmsg);
 883 
 884-    snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
 885+    snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
 886 }
 887 
 888 
 889-void enqueue_request_replies(snac *user, const char *id)
 890-/* enqueues a request for the replies of a message */
 891+void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
 892+/* enqueues an actor refresh */
 893 {
 894-    /* test first if this precise request is already in the queue */
 895-    xs *queue = user_queue(user);
 896-    xs_list *p = queue;
 897-    xs_str *v;
 898-
 899-    while (xs_list_iter(&p, &v)) {
 900-        xs *q_item = queue_get(v);
 901-
 902-        if (q_item != NULL) {
 903-            const char *type = xs_dict_get(q_item, "type");
 904-            const char *msg  = xs_dict_get(q_item, "message");
 905-
 906-            if (type && msg && strcmp(type, "request_replies") == 0 && strcmp(msg, id) == 0) {
 907-                /* don't requeue */
 908-                snac_debug(user, 1, xs_fmt("enqueue_request_replies already here %s", id));
 909-                return;
 910-            }
 911-        }
 912-    }
 913-
 914-    /* not there; enqueue the request with a small delay */
 915-    xs *qmsg = _new_qmsg("request_replies", id, 0);
 916-    xs *ntid = tid(10);
 917+    xs *qmsg = _new_qmsg("actor_refresh", "", 0);
 918+    xs *ntid = tid(forward_secs);
 919     xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
 920 
 921     qmsg = xs_dict_set(qmsg, "ntid", ntid);
 922+    qmsg = xs_dict_append(qmsg, "actor", actor);
 923 
 924     qmsg = _enqueue_put(fn, qmsg);
 925 
 926-    snac_debug(user, 2, xs_fmt("enqueue_request_replies %s", id));
 927+    snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
 928 }
 929 
 930 
 931@@ -2528,14 +2848,14 @@ int was_question_voted(snac *user, const char *id)
 932     xs *children = object_children(id);
 933     int voted = 0;
 934     xs_list *p;
 935-    xs_str *md5;
 936+    const xs_str *md5;
 937 
 938     p = children;
 939     while (xs_list_iter(&p, &md5)) {
 940         xs *obj = NULL;
 941 
 942         if (valid_status(object_get_by_md5(md5, &obj))) {
 943-            char *atto = get_atto(obj);
 944+            const char *atto = get_atto(obj);
 945             if (atto && strcmp(atto, user->actor) == 0 &&
 946                 !xs_is_null(xs_dict_get(obj, "name"))) {
 947                 voted = 1;
 948@@ -2555,7 +2875,7 @@ xs_list *user_queue(snac *snac)
 949     xs_list *list = xs_list_new();
 950     time_t t      = time(NULL);
 951     xs_list *p;
 952-    xs_val *v;
 953+    const xs_val *v;
 954 
 955     xs *fns = xs_glob(spec, 0, 0);
 956 
 957@@ -2584,7 +2904,7 @@ xs_list *queue(void)
 958     xs_list *list = xs_list_new();
 959     time_t t      = time(NULL);
 960     xs_list *p;
 961-    xs_val *v;
 962+    const xs_val *v;
 963 
 964     xs *fns = xs_glob(spec, 0, 0);
 965 
 966@@ -2660,7 +2980,7 @@ static void _purge_dir(const char *dir, int days)
 967         xs *spec  = xs_fmt("%s/" "*", dir);
 968         xs *list  = xs_glob(spec, 0, 0);
 969         xs_list *p;
 970-        xs_str *v;
 971+        const xs_str *v;
 972 
 973         p = list;
 974         while (xs_list_iter(&p, &v))
 975@@ -2686,7 +3006,7 @@ void purge_server(void)
 976     xs *spec = xs_fmt("%s/object/??", srv_basedir);
 977     xs *dirs = xs_glob(spec, 0, 0);
 978     xs_list *p;
 979-    xs_str *v;
 980+    const xs_str *v;
 981     int cnt = 0;
 982     int icnt = 0;
 983 
 984@@ -2695,7 +3015,7 @@ void purge_server(void)
 985     p = dirs;
 986     while (xs_list_iter(&p, &v)) {
 987         xs_list *p2;
 988-        xs_str *v2;
 989+        const xs_str *v2;
 990 
 991         {
 992             xs *spec2 = xs_fmt("%s/" "*.json", v);
 993@@ -2709,7 +3029,7 @@ void purge_server(void)
 994                 if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
 995                     xs *s1    = xs_replace(v2, ".json", "");
 996                     xs *l     = xs_split(s1, "/");
 997-                    char *md5 = xs_list_get(l, -1);
 998+                    const char *md5 = xs_list_get(l, -1);
 999 
1000                     object_del_by_md5(md5);
1001                     cnt++;
1002@@ -2743,6 +3063,16 @@ void purge_server(void)
1003                     }
1004                 }
1005             }
1006+
1007+            /* delete index backups */
1008+            xs *specb = xs_fmt("%s/" "*.bak", v);
1009+            xs *bakfs = xs_glob(specb, 0, 0);
1010+
1011+            p2 = bakfs;
1012+            while (xs_list_iter(&p2, &v2)) {
1013+                unlink(v2);
1014+                srv_debug(1, xs_fmt("purged %s", v2));
1015+            }
1016         }
1017     }
1018 
1019@@ -2764,7 +3094,7 @@ void purge_server(void)
1020         xs *spec2 = xs_fmt("%s/" "*.idx", v);
1021         xs *files = xs_glob(spec2, 0, 0);
1022         xs_list *p2;
1023-        xs_str *v2;
1024+        const xs_str *v2;
1025 
1026         p2 = files;
1027         while (xs_list_iter(&p2, &v2)) {
1028@@ -2791,7 +3121,7 @@ void purge_user(snac *snac)
1029 /* do the purge for this user */
1030 {
1031     int priv_days, pub_days, user_days = 0;
1032-    char *v;
1033+    const char *v;
1034     int n;
1035 
1036     priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
1037@@ -2823,6 +3153,19 @@ void purge_user(snac *snac)
1038         srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
1039     }
1040 
1041+    /* purge lists */
1042+    {
1043+        xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
1044+        xs *lol  = xs_glob(spec, 0, 0);
1045+        int c = 0;
1046+        const char *v;
1047+
1048+        while (xs_list_next(lol, &v, &c)) {
1049+            int gc = index_gc(v);
1050+            srv_debug(1, xs_fmt("purge: %s %d", v, gc));
1051+        }
1052+    }
1053+
1054     /* unrelated to purging, but it's a janitorial process, so what the hell */
1055     verify_links(snac);
1056 }
1057@@ -2833,7 +3176,8 @@ void purge_all(void)
1058 {
1059     snac snac;
1060     xs *list = user_list();
1061-    char *p, *uid;
1062+    char *p;
1063+    const char *uid;
1064 
1065     p = list;
1066     while (xs_list_iter(&p, &uid)) {
1067@@ -2887,7 +3231,7 @@ void srv_archive(const char *direction, const char *url, xs_dict *req,
1068         if (p_size && payload) {
1069             xs *payload_fn = NULL;
1070             xs *payload_fn_raw = NULL;
1071-            char *v = xs_dict_get(req, "content-type");
1072+            const char *v = xs_dict_get(req, "content-type");
1073 
1074             if (v && xs_str_in(v, "json") != -1) {
1075                 payload_fn = xs_fmt("%s/payload.json", dir);
1076@@ -2918,7 +3262,7 @@ void srv_archive(const char *direction, const char *url, xs_dict *req,
1077 
1078         if (b_size && body) {
1079             xs *body_fn = NULL;
1080-            char *v = xs_dict_get(headers, "content-type");
1081+            const char *v = xs_dict_get(headers, "content-type");
1082 
1083             if (v && xs_str_in(v, "json") != -1) {
1084                 body_fn = xs_fmt("%s/body.json", dir);
1085@@ -2987,7 +3331,7 @@ void srv_archive_error(const char *prefix, const xs_str *err,
1086 }
1087 
1088 
1089-void srv_archive_qitem(char *prefix, xs_dict *q_item)
1090+void srv_archive_qitem(const char *prefix, xs_dict *q_item)
1091 /* archives a q_item in the error folder */
1092 {
1093     xs *ntid = tid(0);
+15, -0
 1@@ -143,6 +143,14 @@ times the sending will be retried.
 2 The number of minutes to wait before the failed posting of a message is
 3 retried. This is not linear, but multipled by the number of retries
 4 already done.
 5+.It Ic queue_timeout
 6+The maximum number of seconds to wait when sending a message from the queue.
 7+.It Ic queue_timeout_2
 8+The maximum number of seconds to wait when sending a message from the queue
 9+to those servers that went timeout in the previous retry. If you want to
10+give slow servers a chance to receive your messages, you can increase this
11+value (but also take into account that processing the queue will take longer
12+while waiting for these molasses to respond).
13 .It Ic max_timeline_entries
14 This is the maximum timeline entries shown in the web interface.
15 .It Ic timeline_purge_days
16@@ -209,6 +217,13 @@ with a large number of users.
17 If this numeric value (in seconds) is set, any activity coming from an account
18 that was created more recently than that will be rejected. This may be used
19 to mitigate spam from automatically created accounts.
20+.It Ic protocol
21+This string value contains the protocol (schema) to be used in URLs. If not
22+set, it defaults to "https". If you run
23+.Nm
24+as part of a hidden network like Tor or I2P that doesn't have a TLS /
25+Certificate infrastructure, you need to set it to "http". Don't change it
26+unless you know what you are doing.
27 .El
28 .Pp
29 You must restart the server to make effective these changes.
+11, -8
 1@@ -82,7 +82,8 @@ static xs_str *format_line(const char *line, xs_list **attach)
 2 /* formats a line */
 3 {
 4     xs_str *s = xs_str_new(NULL);
 5-    char *p, *v;
 6+    char *p;
 7+    const char *v;
 8 
 9     /* split by markup */
10     xs *sm = xs_regex_split(line,
11@@ -155,7 +156,8 @@ xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag
12     int in_pre = 0;
13     int in_blq = 0;
14     xs *list;
15-    char *p, *v;
16+    char *p;
17+    const char *v;
18 
19     /* work by lines */
20     list = xs_split(content, "\n");
21@@ -234,14 +236,14 @@ xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag
22         /* traditional emoticons */
23         xs *d = emojis();
24         int c = 0;
25-        char *k, *v;
26+        const char *k, *v;
27 
28         while (xs_dict_next(d, &k, &v, &c)) {
29             const char *t = NULL;
30 
31             /* is it an URL to an image? */
32             if (xs_startswith(v, "https:/" "/") && xs_startswith((t = xs_mime_by_ext(v)), "image/")) {
33-                if (tag) {
34+                if (tag && xs_str_in(s, k) != -1) {
35                     /* add the emoji to the tag list */
36                     xs *e = xs_dict_new();
37                     xs *i = xs_dict_new();
38@@ -280,7 +282,8 @@ xs_str *sanitize(const char *content)
39     xs_str *s = xs_str_new(NULL);
40     xs *sl;
41     int n = 0;
42-    char *p, *v;
43+    char *p;
44+    const char *v;
45 
46     sl = xs_regex_split(content, "</?[^>]+>");
47 
48@@ -311,9 +314,9 @@ xs_str *sanitize(const char *content)
49 
50                 s = xs_str_cat(s, s2);
51             } else {
52-                /* else? just show it with encoded code.. that's it. */
53-                xs *el = encode_html(v);
54-                s = xs_str_cat(s, el);
55+                /* treat end of divs as paragraph breaks */
56+                if (strcmp(v, "</div>"))
57+                    s = xs_str_cat(s, "<p>");
58             }
59         }
60         else {
M html.c
+453, -250
   1@@ -41,7 +41,7 @@ int login(snac *snac, const xs_dict *headers)
   2 }
   3 
   4 
   5-xs_str *replace_shortnames(xs_str *s, xs_list *tag, int ems)
   6+xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems)
   7 /* replaces all the :shortnames: with the emojis in tag */
   8 {
   9     if (!xs_is_null(tag)) {
  10@@ -55,20 +55,20 @@ xs_str *replace_shortnames(xs_str *s, xs_list *tag, int ems)
  11             tag_list = xs_dup(tag);
  12         }
  13 
  14-        xs *style = xs_fmt("height: %dem; vertical-align: middle;", ems);
  15+        xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems);
  16 
  17-        xs_list *p = tag_list;
  18-        char *v;
  19+        const char *v;
  20+        int c = 0;
  21 
  22-        while (xs_list_iter(&p, &v)) {
  23-            char *t = xs_dict_get(v, "type");
  24+        while (xs_list_next(tag_list, &v, &c)) {
  25+            const char *t = xs_dict_get(v, "type");
  26 
  27             if (t && strcmp(t, "Emoji") == 0) {
  28-                char *n = xs_dict_get(v, "name");
  29-                char *i = xs_dict_get(v, "icon");
  30+                const char *n = xs_dict_get(v, "name");
  31+                const char *i = xs_dict_get(v, "icon");
  32 
  33                 if (n && i) {
  34-                    char *u = xs_dict_get(i, "url");
  35+                    const char *u = xs_dict_get(i, "url");
  36                     xs_html *img = xs_html_sctag("img",
  37                         xs_html_attr("loading", "lazy"),
  38                         xs_html_attr("src", u),
  39@@ -88,7 +88,7 @@ xs_str *replace_shortnames(xs_str *s, xs_list *tag, int ems)
  40 xs_str *actor_name(xs_dict *actor)
  41 /* gets the actor name */
  42 {
  43-    char *v;
  44+    const char *v;
  45 
  46     if (xs_is_null((v = xs_dict_get(actor, "name"))) || *v == '\0') {
  47         if (xs_is_null(v = xs_dict_get(actor, "preferredUsername")) || *v == '\0') {
  48@@ -106,7 +106,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
  49     xs_html *actor_icon = xs_html_tag("p", NULL);
  50 
  51     xs *avatar = NULL;
  52-    char *v;
  53+    const char *v;
  54     int fwing = 0;
  55     int fwer = 0;
  56 
  57@@ -125,7 +125,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
  58     if (avatar == NULL)
  59         avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
  60 
  61-    char *actor_id = xs_dict_get(actor, "id");
  62+    const char *actor_id = xs_dict_get(actor, "id");
  63     xs *href = NULL;
  64 
  65     if (user) {
  66@@ -216,7 +216,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
  67     }
  68 
  69     {
  70-        char *username, *id;
  71+        const char *username, *id;
  72 
  73         if (xs_is_null(username = xs_dict_get(actor, "preferredUsername")) || *username == '\0') {
  74             /* This should never be reached */
  75@@ -244,19 +244,19 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
  76 }
  77 
  78 
  79-xs_html *html_msg_icon(snac *user, char *actor_id, const xs_dict *msg)
  80+xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg)
  81 {
  82     xs *actor = NULL;
  83     xs_html *actor_icon = NULL;
  84 
  85     if (actor_id && valid_status(actor_get_refresh(user, actor_id, &actor))) {
  86-        char *date  = NULL;
  87-        char *udate = NULL;
  88-        char *url   = NULL;
  89+        const char *date  = NULL;
  90+        const char *udate = NULL;
  91+        const char *url   = NULL;
  92         int priv    = 0;
  93         const char *type = xs_dict_get(msg, "type");
  94 
  95-        if (xs_match(type, "Note|Question|Page|Article|Video"))
  96+        if (xs_match(type, POSTLIKE_OBJECT_TYPE))
  97             url = xs_dict_get(msg, "id");
  98 
  99         priv = !is_msg_public(msg);
 100@@ -271,14 +271,14 @@ xs_html *html_msg_icon(snac *user, char *actor_id, const xs_dict *msg)
 101 }
 102 
 103 
 104-xs_html *html_note(snac *user, char *summary,
 105-                   char *div_id, char *form_id,
 106-                   char *ta_plh, char *ta_content,
 107-                   char *edit_id, char *actor_id,
 108-                   xs_val *cw_yn, char *cw_text,
 109-                   xs_val *mnt_only, char *redir,
 110-                   char *in_reply_to, int poll,
 111-                   char *att_file, char *att_alt_text)
 112+xs_html *html_note(snac *user, const char *summary,
 113+                   const char *div_id, const char *form_id,
 114+                   const char *ta_plh, const char *ta_content,
 115+                   const char *edit_id, const char *actor_id,
 116+                   const xs_val *cw_yn, const char *cw_text,
 117+                   const xs_val *mnt_only, const char *redir,
 118+                   const char *in_reply_to, int poll,
 119+                   const char *att_file, const char *att_alt_text)
 120 {
 121     xs *action = xs_fmt("%s/admin/note", user->actor);
 122 
 123@@ -460,9 +460,11 @@ static xs_html *html_base_head(void)
 124     /* add server CSS and favicon */
 125     xs *f;
 126     f = xs_fmt("%s/favicon.ico", srv_baseurl);
 127-    xs_list *p = xs_dict_get(srv_config, "cssurls");
 128-    char *v;
 129-    while (xs_list_iter(&p, &v)) {
 130+    const xs_list *p = xs_dict_get(srv_config, "cssurls");
 131+    const char *v;
 132+    int c = 0;
 133+
 134+    while (xs_list_next(p, &v, &c)) {
 135         xs_html_add(head,
 136             xs_html_sctag("link",
 137                 xs_html_attr("rel",  "stylesheet"),
 138@@ -498,8 +500,8 @@ xs_html *html_instance_head(void)
 139         }
 140     }
 141 
 142-    char *host  = xs_dict_get(srv_config, "host");
 143-    char *title = xs_dict_get(srv_config, "title");
 144+    const char *host  = xs_dict_get(srv_config, "host");
 145+    const char *title = xs_dict_get(srv_config, "title");
 146 
 147     xs_html_add(head,
 148         xs_html_tag("title",
 149@@ -509,12 +511,12 @@ xs_html *html_instance_head(void)
 150 }
 151 
 152 
 153-static xs_html *html_instance_body(char *tag)
 154+static xs_html *html_instance_body(void)
 155 {
 156-    char *host  = xs_dict_get(srv_config, "host");
 157-    char *sdesc = xs_dict_get(srv_config, "short_description");
 158-    char *email = xs_dict_get(srv_config, "admin_email");
 159-    char *acct  = xs_dict_get(srv_config, "admin_account");
 160+    const char *host  = xs_dict_get(srv_config, "host");
 161+    const char *sdesc = xs_dict_get(srv_config, "short_description");
 162+    const char *email = xs_dict_get(srv_config, "admin_email");
 163+    const char *acct  = xs_dict_get(srv_config, "admin_account");
 164 
 165     xs *blurb = xs_replace(snac_blurb, "%host%", host);
 166 
 167@@ -560,16 +562,6 @@ static xs_html *html_instance_body(char *tag)
 168                         xs_html_text(handle)))));
 169     }
 170 
 171-    {
 172-        xs *l = tag ? xs_fmt(L("Search results for #%s"), tag) :
 173-            xs_dup(L("Recent posts by users in this instance"));
 174-
 175-        xs_html_add(body,
 176-            xs_html_tag("h2",
 177-                xs_html_attr("class", "snac-header"),
 178-                xs_html_text(l)));
 179-    }
 180-
 181     return body;
 182 }
 183 
 184@@ -749,7 +741,17 @@ static xs_html *html_user_body(snac *user, int read_only)
 185             xs_html_text(" - "),
 186             xs_html_tag("a",
 187                 xs_html_attr("href", instance_url),
 188-                xs_html_text(L("instance"))));
 189+                xs_html_text(L("instance"))),
 190+            xs_html_text(" "),
 191+            xs_html_tag("form",
 192+                xs_html_attr("style", "display: inline!important"),
 193+                xs_html_attr("class", "snac-search-box"),
 194+                xs_html_attr("action", admin_url),
 195+                    xs_html_sctag("input",
 196+                        xs_html_attr("type", "text"),
 197+                        xs_html_attr("name", "q"),
 198+                        xs_html_attr("title", L("Search posts by content (regular expression) or #tag")),
 199+                        xs_html_attr("placeholder", L("Content search")))));
 200     }
 201 
 202     xs_html_add(body,
 203@@ -760,7 +762,7 @@ static xs_html *html_user_body(snac *user, int read_only)
 204         xs_html_attr("class", "h-card snac-top-user"));
 205 
 206     if (read_only) {
 207-        char *header = xs_dict_get(user->config, "header");
 208+        const char *header = xs_dict_get(user->config, "header");
 209         if (header && *header) {
 210             xs_html_add(top_user,
 211                 xs_html_tag("div",
 212@@ -797,10 +799,10 @@ static xs_html *html_user_body(snac *user, int read_only)
 213         xs_html_add(top_user,
 214             top_user_bio);
 215 
 216-        xs_dict *metadata = xs_dict_get(user->config, "metadata");
 217+        const xs_dict *metadata = xs_dict_get(user->config, "metadata");
 218         if (xs_type(metadata) == XSTYPE_DICT) {
 219-            xs_str *k;
 220-            xs_str *v;
 221+            const xs_str *k;
 222+            const xs_str *v;
 223 
 224             xs_dict *val_links = user->links;
 225             if (xs_is_null(val_links))
 226@@ -813,10 +815,10 @@ static xs_html *html_user_body(snac *user, int read_only)
 227             while (xs_dict_next(metadata, &k, &v, &c)) {
 228                 xs_html *value;
 229 
 230-                if (xs_startswith(v, "https:/" "/")) {
 231+                if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/")) {
 232                     /* is this link validated? */
 233                     xs *verified_link = NULL;
 234-                    xs_number *val_time = xs_dict_get(val_links, v);
 235+                    const xs_number *val_time = xs_dict_get(val_links, v);
 236 
 237                     if (xs_type(val_time) == XSTYPE_NUMBER) {
 238                         time_t t = xs_number_get(val_time);
 239@@ -928,7 +930,7 @@ xs_html *html_top_controls(snac *snac)
 240 
 241     /** user settings **/
 242 
 243-    char *email = "[disabled by admin]";
 244+    const char *email = "[disabled by admin]";
 245 
 246     if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
 247         email = xs_dict_get(snac->config_o, "email");
 248@@ -940,40 +942,40 @@ xs_html *html_top_controls(snac *snac)
 249         }
 250     }
 251 
 252-    char *cw = xs_dict_get(snac->config, "cw");
 253+    const char *cw = xs_dict_get(snac->config, "cw");
 254     if (xs_is_null(cw))
 255         cw = "";
 256 
 257-    char *telegram_bot = xs_dict_get(snac->config, "telegram_bot");
 258+    const char *telegram_bot = xs_dict_get(snac->config, "telegram_bot");
 259     if (xs_is_null(telegram_bot))
 260         telegram_bot = "";
 261 
 262-    char *telegram_chat_id = xs_dict_get(snac->config, "telegram_chat_id");
 263+    const char *telegram_chat_id = xs_dict_get(snac->config, "telegram_chat_id");
 264     if (xs_is_null(telegram_chat_id))
 265         telegram_chat_id = "";
 266 
 267-    char *ntfy_server = xs_dict_get(snac->config, "ntfy_server");
 268+    const char *ntfy_server = xs_dict_get(snac->config, "ntfy_server");
 269     if (xs_is_null(ntfy_server))
 270         ntfy_server = "";
 271 
 272-    char *ntfy_token = xs_dict_get(snac->config, "ntfy_token");
 273+    const char *ntfy_token = xs_dict_get(snac->config, "ntfy_token");
 274     if (xs_is_null(ntfy_token))
 275         ntfy_token = "";
 276 
 277-    char *purge_days = xs_dict_get(snac->config, "purge_days");
 278+    const char *purge_days = xs_dict_get(snac->config, "purge_days");
 279     if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER)
 280         purge_days = (char *)xs_number_str(purge_days);
 281     else
 282         purge_days = "0";
 283 
 284-    xs_val *d_dm_f_u  = xs_dict_get(snac->config, "drop_dm_from_unknown");
 285-    xs_val *bot       = xs_dict_get(snac->config, "bot");
 286-    xs_val *a_private = xs_dict_get(snac->config, "private");
 287+    const xs_val *d_dm_f_u  = xs_dict_get(snac->config, "drop_dm_from_unknown");
 288+    const xs_val *bot       = xs_dict_get(snac->config, "bot");
 289+    const xs_val *a_private = xs_dict_get(snac->config, "private");
 290 
 291     xs *metadata = xs_str_new(NULL);
 292-    xs_dict *md = xs_dict_get(snac->config, "metadata");
 293-    xs_str *k;
 294-    xs_str *v;
 295+    const xs_dict *md = xs_dict_get(snac->config, "metadata");
 296+    const xs_str *k;
 297+    const xs_str *v;
 298 
 299     int c = 0;
 300     while (xs_dict_next(md, &k, &v, &c)) {
 301@@ -1158,13 +1160,14 @@ xs_str *build_mentions(snac *snac, const xs_dict *msg)
 302 /* returns a string with the mentions in msg */
 303 {
 304     xs_str *s = xs_str_new(NULL);
 305-    char *list = xs_dict_get(msg, "tag");
 306-    char *v;
 307+    const char *list = xs_dict_get(msg, "tag");
 308+    const char *v;
 309+    int c = 0;
 310 
 311-    while (xs_list_iter(&list, &v)) {
 312-        char *type = xs_dict_get(v, "type");
 313-        char *href = xs_dict_get(v, "href");
 314-        char *name = xs_dict_get(v, "name");
 315+    while (xs_list_next(list, &v, &c)) {
 316+        const char *type = xs_dict_get(v, "type");
 317+        const char *href = xs_dict_get(v, "href");
 318+        const char *name = xs_dict_get(v, "name");
 319 
 320         if (type && strcmp(type, "Mention") == 0 &&
 321             href && strcmp(href, snac->actor) != 0 && name) {
 322@@ -1208,10 +1211,11 @@ xs_str *build_mentions(snac *snac, const xs_dict *msg)
 323 }
 324 
 325 
 326-xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const char *md5)
 327+xs_html *html_entry_controls(snac *snac, const char *actor,
 328+                            const xs_dict *msg, const char *md5)
 329 {
 330-    char *id    = xs_dict_get(msg, "id");
 331-    char *group = xs_dict_get(msg, "audience");
 332+    const char *id    = xs_dict_get(msg, "id");
 333+    const char *group = xs_dict_get(msg, "audience");
 334 
 335     xs *likes   = object_likes(id);
 336     xs *boosts  = object_announces(id);
 337@@ -1265,8 +1269,8 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
 338     }
 339 
 340     if (is_msg_public(msg)) {
 341-        if (strcmp(actor, snac->actor) == 0 || xs_list_in(boosts, snac->md5) == -1) {
 342-            /* not already boosted or us; add button */
 343+        if (xs_list_in(boosts, snac->md5) == -1) {
 344+            /* not already boosted; add button */
 345             xs_html_add(form,
 346                 html_button("boost", L("Boost"), L("Announce this post to your followers")));
 347         }
 348@@ -1310,7 +1314,7 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
 349         html_button("delete", L("Delete"), L("Delete this post")),
 350         html_button("hide",   L("Hide"), L("Hide this post and its children")));
 351 
 352-    char *prev_src = xs_dict_get(msg, "sourceContent");
 353+    const char *prev_src = xs_dict_get(msg, "sourceContent");
 354 
 355     if (!xs_is_null(prev_src) && strcmp(actor, snac->actor) == 0) { /** edit **/
 356         /* post can be edited */
 357@@ -1318,13 +1322,13 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
 358         xs *form_id = xs_fmt("%s_edit_form", md5);
 359         xs *redir   = xs_fmt("%s_entry", md5);
 360 
 361-        char *att_file = "";
 362-        char *att_alt_text = "";
 363-        xs_list *att_list = xs_dict_get(msg, "attachment");
 364+        const char *att_file = "";
 365+        const char *att_alt_text = "";
 366+        const xs_list *att_list = xs_dict_get(msg, "attachment");
 367 
 368         /* does it have an attachment? */
 369         if (xs_type(att_list) == XSTYPE_LIST && xs_list_len(att_list)) {
 370-            xs_dict *d = xs_list_get(att_list, 0);
 371+            const xs_dict *d = xs_list_get(att_list, 0);
 372 
 373             if (xs_type(d) == XSTYPE_DICT) {
 374                 att_file = xs_dict_get_def(d, "url", "");
 375@@ -1368,12 +1372,13 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const
 376 
 377 
 378 xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 379-                   int level, char *md5, int hide_children)
 380+                   int level, const char *md5, int hide_children)
 381 {
 382-    char *id    = xs_dict_get(msg, "id");
 383-    char *type  = xs_dict_get(msg, "type");
 384-    char *actor;
 385-    char *v;
 386+    const char *id    = xs_dict_get(msg, "id");
 387+    const char *type  = xs_dict_get(msg, "type");
 388+    const char *actor;
 389+    const char *v;
 390+    int has_title = 0;
 391 
 392     /* do not show non-public messages in the public timeline */
 393     if ((read_only || !user) && !is_msg_public(msg))
 394@@ -1405,8 +1410,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 395                 html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg)));
 396     }
 397     else
 398-    if (!xs_match(type, "Note|Question|Page|Article|Video")) {
 399+    if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) {
 400         /* skip oddities */
 401+        snac_debug(user, 1, xs_fmt("html_entry: ignoring object type '%s' %s", type, id));
 402         return NULL;
 403     }
 404 
 405@@ -1483,6 +1489,14 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 406         }
 407     }
 408 
 409+    if (strcmp(type, "Event") == 0) {
 410+        /* add the calendar emoji */
 411+        xs_html_add(score,
 412+            xs_html_tag("span",
 413+                xs_html_attr("title", L("Event")),
 414+                xs_html_raw(" &#128197; ")));
 415+    }
 416+
 417     /* if it's a user from this same instance, add the score */
 418     if (xs_startswith(id, srv_baseurl)) {
 419         int n_likes  = object_likes_len(id);
 420@@ -1499,7 +1513,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 421 
 422     if (xs_list_len(boosts)) {
 423         /* if somebody boosted this, show as origin */
 424-        char *p = xs_list_get(boosts, -1);
 425+        const char *p = xs_list_get(boosts, -1);
 426         xs *actor_r = NULL;
 427 
 428         if (user && xs_list_in(boosts, user->md5) != -1) {
 429@@ -1519,7 +1533,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 430 
 431             if (!xs_is_null(name)) {
 432                 xs *href = NULL;
 433-                char *id = xs_dict_get(actor_r, "id");
 434+                const char *id = xs_dict_get(actor_r, "id");
 435                 int fwers = 0;
 436                 int fwing = 0;
 437 
 438@@ -1548,7 +1562,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 439     if (strcmp(type, "Note") == 0) {
 440         if (level == 0) {
 441             /* is the parent not here? */
 442-            char *parent = xs_dict_get(msg, "inReplyTo");
 443+            const char *parent = xs_dict_get(msg, "inReplyTo");
 444 
 445             if (user && !xs_is_null(parent) && *parent && !timeline_here(user, parent)) {
 446                 xs_html_add(post_header,
 447@@ -1574,11 +1588,13 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 448     xs_html_add(entry,
 449         snac_content_wrap);
 450 
 451-    if (!xs_is_null(v = xs_dict_get(msg, "name"))) {
 452+    if (!has_title && !xs_is_null(v = xs_dict_get(msg, "name"))) {
 453         xs_html_add(snac_content_wrap,
 454             xs_html_tag("h3",
 455                 xs_html_attr("class", "snac-entry-title"),
 456                 xs_html_text(v)));
 457+
 458+        has_title = 1;
 459     }
 460 
 461     xs_html *snac_content = NULL;
 462@@ -1591,7 +1607,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 463             v = "...";
 464 
 465         /* only show it when not in the public timeline and the config setting is "open" */
 466-        char *cw = xs_dict_get(user->config, "cw");
 467+        const char *cw = xs_dict_get(user->config, "cw");
 468         if (xs_is_null(cw) || read_only)
 469             cw = "";
 470 
 471@@ -1603,12 +1619,15 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 472     }
 473     else {
 474         /* print the summary as a header (sites like e.g. Friendica can contain one) */
 475-        if (!xs_is_null(v) && *v)
 476+        if (!has_title && !xs_is_null(v) && *v) {
 477             xs_html_add(snac_content_wrap,
 478                 xs_html_tag("h3",
 479                     xs_html_attr("class", "snac-entry-title"),
 480                     xs_html_text(v)));
 481 
 482+            has_title = 1;
 483+        }
 484+
 485         snac_content = xs_html_tag("div", NULL);
 486     }
 487 
 488@@ -1617,7 +1636,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 489 
 490     {
 491         /** build the content string **/
 492-        char *content = xs_dict_get(msg, "content");
 493+        const char *content = xs_dict_get(msg, "content");
 494 
 495         xs *c = sanitize(xs_is_null(content) ? "" : content);
 496 
 497@@ -1635,7 +1654,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 498         c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2);
 499 
 500         /* Peertube videos content is in markdown */
 501-        char *mtype = xs_dict_get(msg, "mediaType");
 502+        const char *mtype = xs_dict_get(msg, "mediaType");
 503         if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "text/markdown") == 0) {
 504             /* a full conversion could be better */
 505             c = xs_replace_i(c, "\r", "");
 506@@ -1648,26 +1667,33 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 507     }
 508 
 509     if (strcmp(type, "Question") == 0) { /** question content **/
 510-        xs_list *oo = xs_dict_get(msg, "oneOf");
 511-        xs_list *ao = xs_dict_get(msg, "anyOf");
 512-        xs_list *p;
 513-        xs_dict *v;
 514+        const xs_list *oo = xs_dict_get(msg, "oneOf");
 515+        const xs_list *ao = xs_dict_get(msg, "anyOf");
 516+        const xs_list *p;
 517+        const xs_dict *v;
 518         int closed = 0;
 519+        const char *f_closed = NULL;
 520 
 521         xs_html *poll = xs_html_tag("div", NULL);
 522 
 523         if (read_only)
 524             closed = 1; /* non-identified page; show as closed */
 525         else
 526-        if (xs_dict_get(msg, "closed"))
 527-            closed = 2;
 528-        else
 529         if (user && xs_startswith(id, user->actor))
 530             closed = 1; /* we questioned; closed for us */
 531         else
 532         if (user && was_question_voted(user, id))
 533             closed = 1; /* we already voted; closed for us */
 534 
 535+        if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
 536+            /* it has a closed date... but is it in the past? */
 537+            time_t t0 = time(NULL);
 538+            time_t t1 = xs_parse_iso_date(f_closed, 0);
 539+
 540+            if (t1 < t0)
 541+                closed = 2;
 542+        }
 543+
 544         /* get the appropriate list of options */
 545         p = oo != NULL ? oo : ao;
 546 
 547@@ -1675,10 +1701,11 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 548             /* closed poll */
 549             xs_html *poll_result = xs_html_tag("table",
 550                 xs_html_attr("class", "snac-poll-result"));
 551+            int c = 0;
 552 
 553-            while (xs_list_iter(&p, &v)) {
 554-                char *name       = xs_dict_get(v, "name");
 555-                xs_dict *replies = xs_dict_get(v, "replies");
 556+            while (xs_list_next(p, &v, &c)) {
 557+                const char *name       = xs_dict_get(v, "name");
 558+                const xs_dict *replies = xs_dict_get(v, "replies");
 559 
 560                 if (name && replies) {
 561                     char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
 562@@ -1715,9 +1742,10 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 563                         xs_html_attr("name", "irt"),
 564                         xs_html_attr("value", id))));
 565 
 566-            while (xs_list_iter(&p, &v)) {
 567-                char *name = xs_dict_get(v, "name");
 568-                xs_dict *replies = xs_dict_get(v, "replies");
 569+            int c = 0;
 570+            while (xs_list_next(p, &v, &c)) {
 571+                const char *name = xs_dict_get(v, "name");
 572+                const xs_dict *replies = xs_dict_get(v, "replies");
 573 
 574                 if (name) {
 575                     char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
 576@@ -1755,7 +1783,13 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 577         }
 578         else {
 579             /* show when the poll closes */
 580-            char *end_time = xs_dict_get(msg, "endTime");
 581+            const char *end_time = xs_dict_get(msg, "endTime");
 582+
 583+            /* Pleroma does not have an endTime field;
 584+               it has a closed time in the future */
 585+            if (xs_is_null(end_time))
 586+                end_time = xs_dict_get(msg, "closed");
 587+
 588             if (!xs_is_null(end_time)) {
 589                 time_t t0 = time(NULL);
 590                 time_t t1 = xs_parse_iso_date(end_time, 0);
 591@@ -1792,12 +1826,12 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 592         xs_html_add(snac_content,
 593             content_attachments);
 594 
 595-        xs_list *p = attach;
 596-
 597-        while (xs_list_iter(&p, &v)) {
 598-            char *type = xs_dict_get(v, "type");
 599-            char *href = xs_dict_get(v, "href");
 600-            char *name = xs_dict_get(v, "name");
 601+        int c = 0;
 602+        const xs_dict *a;
 603+        while (xs_list_next(attach, &a, &c)) {
 604+            const char *type = xs_dict_get(a, "type");
 605+            const char *href = xs_dict_get(a, "href");
 606+            const char *name = xs_dict_get(a, "name");
 607 
 608             if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
 609                 xs_html_add(content_attachments,
 610@@ -1861,7 +1895,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 611     }
 612 
 613     /* has this message an audience (i.e., comes from a channel or community)? */
 614-    char *audience = xs_dict_get(msg, "audience");
 615+    const char *audience = xs_dict_get(msg, "audience");
 616     if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
 617         xs_html *au_tag = xs_html_tag("p",
 618             xs_html_text("("),
 619@@ -1911,7 +1945,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 620             }
 621 
 622             xs_list *p = children;
 623-            char *cmd5;
 624+            const char *cmd5;
 625             int cnt = 0;
 626             int o_cnt = 0;
 627 
 628@@ -1983,11 +2017,11 @@ xs_html *html_footer(void)
 629 
 630 xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
 631                       int skip, int show, int show_more,
 632-                      char *tag, char *page, int utl)
 633+                      char *title, char *page, int utl)
 634 /* returns the HTML for the timeline */
 635 {
 636     xs_list *p = (xs_list *)list;
 637-    char *v;
 638+    const char *v;
 639     double t = ftime();
 640 
 641     xs *desc = NULL;
 642@@ -1995,11 +2029,12 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
 643 
 644     if (xs_list_len(list) == 1) {
 645         /* only one element? pick the description from the source */
 646-        char *id = xs_list_get(list, 0);
 647+        const char *id = xs_list_get(list, 0);
 648         xs *d = NULL;
 649         object_get_by_md5(id, &d);
 650-        if (d && (v = xs_dict_get(d, "sourceContent")) != NULL)
 651-            desc = xs_dup(v);
 652+        const char *sc = xs_dict_get(d, "sourceContent");
 653+        if (d && sc != NULL)
 654+            desc = xs_dup(sc);
 655 
 656         alternate = xs_dup(xs_dict_get(d, "id"));
 657     }
 658@@ -2013,7 +2048,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
 659     }
 660     else {
 661         head = html_instance_head();
 662-        body = html_instance_body(tag);
 663+        body = html_instance_body();
 664     }
 665 
 666     xs_html *html = xs_html_tag("html",
 667@@ -2024,6 +2059,41 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
 668         xs_html_add(body,
 669             html_top_controls(user));
 670 
 671+    /* show links to the available lists */
 672+    if (user && !read_only) {
 673+        xs *lists = list_maint(user, NULL, 0); /* get list of lists */
 674+
 675+        if (xs_list_len(lists)) {
 676+            int ct = 0;
 677+            const char *v;
 678+
 679+            xs_html *lol = xs_html_tag("ul",
 680+                xs_html_attr("class", "snac-list-of-lists"));
 681+            xs_html_add(body, lol);
 682+
 683+            while (xs_list_next(lists, &v, &ct)) {
 684+                const char *lname = xs_list_get(v, 1);
 685+                xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0));
 686+                xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname);
 687+
 688+                xs_html_add(lol,
 689+                    xs_html_tag("li",
 690+                        xs_html_tag("a",
 691+                            xs_html_attr("href", url),
 692+                            xs_html_attr("class", "snac-list-link"),
 693+                            xs_html_attr("title", ttl),
 694+                            xs_html_text(lname))));
 695+            }
 696+        }
 697+    }
 698+
 699+    if (title) {
 700+        xs_html_add(body,
 701+            xs_html_tag("h2",
 702+                xs_html_attr("class", "snac-header"),
 703+                xs_html_text(title)));
 704+    }
 705+
 706     xs_html_add(body,
 707         xs_html_tag("a",
 708             xs_html_attr("name", "snac-posts")));
 709@@ -2052,11 +2122,18 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
 710 
 711         /* is this message a non-public reply? */
 712         if (user != NULL && !is_msg_public(msg)) {
 713-            char *irt = xs_dict_get(msg, "inReplyTo");
 714+            const char *irt = xs_dict_get(msg, "inReplyTo");
 715 
 716+            /* is it a reply to something not in the storage? */
 717             if (!xs_is_null(irt) && !object_here(irt)) {
 718-                snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
 719-                continue;
 720+                /* is it for me? */
 721+                const xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
 722+                const xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
 723+
 724+                if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
 725+                    snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
 726+                    continue;
 727+                }
 728             }
 729         }
 730 
 731@@ -2081,7 +2158,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
 732 
 733             xs *list = history_list(user);
 734             xs_list *p = list;
 735-            char *v;
 736+            const char *v;
 737 
 738             while (xs_list_iter(&p, &v)) {
 739                 xs *fn  = xs_replace(v, ".html", "");
 740@@ -2106,25 +2183,22 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
 741     }
 742 
 743     if (show_more) {
 744-        xs *t  = NULL;
 745         xs *m  = NULL;
 746         xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
 747 
 748-        xs *url = page == NULL || user == NULL ?
 749-            xs_dup(srv_baseurl) : xs_fmt("%s%s", user->actor, page);
 750+        xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
 751 
 752-        if (tag) {
 753-            t = xs_fmt("%s?t=%s", url, tag);
 754-            m = xs_fmt("%s&%s", t, ss);
 755-        }
 756-        else {
 757-            t = xs_dup(url);
 758-            m = xs_fmt("%s?%s", t, ss);
 759-        }
 760+        if (page != NULL)
 761+            url = xs_str_cat(url, page);
 762+
 763+        if (xs_str_in(url, "?") != -1)
 764+            m = xs_fmt("%s&%s", url, ss);
 765+        else
 766+            m = xs_fmt("%s?%s", url, ss);
 767 
 768         xs_html *more_links = xs_html_tag("p",
 769             xs_html_tag("a",
 770-                xs_html_attr("href", t),
 771+                xs_html_attr("href", url),
 772                 xs_html_attr("name", "snac-more"),
 773                 xs_html_text(L("Back to top"))),
 774             xs_html_text(" - "),
 775@@ -2157,7 +2231,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t)
 776                     xs_html_text("..."))));
 777 
 778     xs_list *p = list;
 779-    char *actor_id;
 780+    const char *actor_id;
 781 
 782     while (xs_list_iter(&p, &actor_id)) {
 783         xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
 784@@ -2173,7 +2247,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t)
 785                     html_actor_icon(snac, actor, xs_dict_get(actor, "published"), NULL, NULL, 0, 1)));
 786 
 787             /* content (user bio) */
 788-            char *c = xs_dict_get(actor, "summary");
 789+            const char *c = xs_dict_get(actor, "summary");
 790 
 791             if (!xs_is_null(c)) {
 792                 xs *sc = sanitize(c);
 793@@ -2317,7 +2391,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
 794     xs_html *noti_seen = NULL;
 795 
 796     xs_list *p = n_list;
 797-    xs_str *v;
 798+    const xs_str *v;
 799     while (xs_list_iter(&p, &v)) {
 800         xs *noti = notify_get(user, v);
 801 
 802@@ -2325,10 +2399,10 @@ xs_str *html_notifications(snac *user, int skip, int show)
 803             continue;
 804 
 805         xs *obj = NULL;
 806-        char *type  = xs_dict_get(noti, "type");
 807-        char *utype = xs_dict_get(noti, "utype");
 808-        char *id    = xs_dict_get(noti, "objid");
 809-        char *date  = xs_dict_get(noti, "date");
 810+        const char *type  = xs_dict_get(noti, "type");
 811+        const char *utype = xs_dict_get(noti, "utype");
 812+        const char *id    = xs_dict_get(noti, "objid");
 813+        const char *date  = xs_dict_get(noti, "date");
 814 
 815         if (xs_is_null(id) || !valid_status(object_get(id, &obj)))
 816             continue;
 817@@ -2336,14 +2410,14 @@ xs_str *html_notifications(snac *user, int skip, int show)
 818         if (is_hidden(user, id))
 819             continue;
 820 
 821-        char *actor_id = xs_dict_get(noti, "actor");
 822+        const char *actor_id = xs_dict_get(noti, "actor");
 823         xs *actor = NULL;
 824 
 825         if (!valid_status(actor_get(actor_id, &actor)))
 826             continue;
 827 
 828         xs *a_name = actor_name(actor);
 829-        char *label = type;
 830+        const char *label = type;
 831 
 832         if (strcmp(type, "Create") == 0)
 833             label = L("Mention");
 834@@ -2455,14 +2529,14 @@ xs_str *html_notifications(snac *user, int skip, int show)
 835 int html_get_handler(const xs_dict *req, const char *q_path,
 836                      char **body, int *b_size, char **ctype, xs_str **etag)
 837 {
 838-    char *accept = xs_dict_get(req, "accept");
 839+    const char *accept = xs_dict_get(req, "accept");
 840     int status = 404;
 841     snac snac;
 842     xs *uid = NULL;
 843-    char *p_path;
 844+    const char *p_path;
 845     int cache = 1;
 846     int save = 1;
 847-    char *v;
 848+    const char *v;
 849 
 850     xs *l = xs_split_n(q_path, "/", 2);
 851     v = xs_list_get(l, 1);
 852@@ -2501,7 +2575,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 853 
 854     int skip = 0;
 855     int show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries"));
 856-    char *q_vars = xs_dict_get(req, "q_vars");
 857+    const xs_dict *q_vars = xs_dict_get(req, "q_vars");
 858     if ((v = xs_dict_get(q_vars, "skip")) != NULL)
 859         skip = atoi(v), cache = 0, save = 0;
 860     if ((v = xs_dict_get(q_vars, "show")) != NULL)
 861@@ -2546,35 +2620,99 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 862             status = 401;
 863         }
 864         else {
 865-            double t = history_mtime(&snac, "timeline.html_");
 866+            const char *q = xs_dict_get(q_vars, "q");
 867+
 868+            if (q && *q) {
 869+                if (*q == '#') {
 870+                    /** search by tag **/
 871+                    xs *tl = tag_search(q, skip, show + 1);
 872+                    int more = 0;
 873+                    if (xs_list_len(tl) >= show + 1) {
 874+                        /* drop the last one */
 875+                        tl = xs_list_del(tl, -1);
 876+                        more = 1;
 877+                    }
 878 
 879-            /* if enabled by admin, return a cached page if its timestamp is:
 880-               a) newer than the timeline timestamp
 881-               b) newer than the start time of the server
 882-            */
 883-            if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) {
 884-                snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
 885+                    xs *page = xs_fmt("/admin?q=%%23%s", q + 1);
 886+                    xs *title = xs_fmt(xs_list_len(tl) ?
 887+                        L("Search results for tag %s") : L("Nothing found for tag %s"), q);
 888 
 889-                status = history_get(&snac, "timeline.html_", body, b_size,
 890-                            xs_dict_get(req, "if-none-match"), etag);
 891+                    *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0);
 892+                    *b_size = strlen(*body);
 893+                    status  = 200;
 894+                }
 895+                else {
 896+                    /** search by content **/
 897+                    int to = 0;
 898+                    int msecs = atoi(xs_dict_get_def(q_vars, "msecs", "0"));
 899+                    xs *tl = content_search(&snac, q, 1, skip, show, msecs, &to);
 900+                    xs *title = NULL;
 901+                    xs *page = xs_fmt("/admin?q=%s&msecs=%d", q, msecs + 10);
 902+                    int tl_len = xs_list_len(tl);
 903+
 904+                    if (tl_len)
 905+                        title = xs_fmt(L("Search results for '%s'"), q);
 906+                    else
 907+                    if (skip)
 908+                        title = xs_fmt(L("No more matches for '%s'"), q);
 909+                    else
 910+                        title = xs_fmt(L("Nothing found for '%s'"), q);
 911+
 912+                    *body   = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show, title, page, 0);
 913+                    *b_size = strlen(*body);
 914+                    status  = 200;
 915+                }
 916             }
 917             else {
 918-                snac_debug(&snac, 1, xs_fmt("building timeline"));
 919+                double t = history_mtime(&snac, "timeline.html_");
 920+
 921+                /* if enabled by admin, return a cached page if its timestamp is:
 922+                   a) newer than the timeline timestamp
 923+                   b) newer than the start time of the server
 924+                */
 925+                if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) {
 926+                    snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
 927 
 928-                xs *list = timeline_list(&snac, "private", skip, show);
 929-                xs *next = timeline_list(&snac, "private", skip + show, 1);
 930+                    status = history_get(&snac, "timeline.html_", body, b_size,
 931+                                xs_dict_get(req, "if-none-match"), etag);
 932+                }
 933+                else {
 934+                    snac_debug(&snac, 1, xs_fmt("building timeline"));
 935+
 936+                    xs *list = timeline_list(&snac, "private", skip, show);
 937+                    xs *next = timeline_list(&snac, "private", skip + show, 1);
 938+
 939+                    xs *pins = pinned_list(&snac);
 940+                    pins = xs_list_cat(pins, list);
 941 
 942-                xs *pins = pinned_list(&snac);
 943-                pins = xs_list_cat(pins, list);
 944+                    *body = html_timeline(&snac, pins, 0, skip, show,
 945+                            xs_list_len(next), NULL, "/admin", 1);
 946 
 947-                *body = html_timeline(&snac, pins, 0, skip, show,
 948-                        xs_list_len(next), NULL, "/admin", 1);
 949+                    *b_size = strlen(*body);
 950+                    status  = 200;
 951 
 952+                    if (save)
 953+                        history_add(&snac, "timeline.html_", *body, *b_size, etag);
 954+                }
 955+            }
 956+        }
 957+    }
 958+    else
 959+    if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
 960+        if (!login(&snac, req)) {
 961+            *body  = xs_dup(uid);
 962+            status = 401;
 963+        }
 964+        else {
 965+            xs *l = xs_split(p_path, "/");
 966+            const char *md5 = xs_list_get(l, -1);
 967+
 968+            if (md5 && *md5 && timeline_here(&snac, md5)) {
 969+                xs *list = xs_list_append(xs_list_new(), md5);
 970+
 971+                *body   = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1);
 972                 *b_size = strlen(*body);
 973                 status  = 200;
 974-
 975-                if (save)
 976-                    history_add(&snac, "timeline.html_", *body, *b_size, etag);
 977             }
 978         }
 979     }
 980@@ -2613,12 +2751,37 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 981             xs *next = timeline_instance_list(skip + show, 1);
 982 
 983             *body = html_timeline(&snac, list, 0, skip, show,
 984-                xs_list_len(next), NULL, "/instance", 0);
 985+                xs_list_len(next), L("Showing instance timeline"), "/instance", 0);
 986             *b_size = strlen(*body);
 987             status  = 200;
 988         }
 989     }
 990     else
 991+    if (xs_startswith(p_path, "list/")) { /** list timelines **/
 992+        if (!login(&snac, req)) {
 993+            *body  = xs_dup(uid);
 994+            status = 401;
 995+        }
 996+        else {
 997+            xs *l = xs_split(p_path, "/");
 998+            const char *lid = xs_list_get(l, -1);
 999+
1000+            xs *list = list_timeline(&snac, lid, skip, show);
1001+            xs *next = list_timeline(&snac, lid, skip + show, 1);
1002+
1003+            if (list != NULL) {
1004+                xs *base = xs_fmt("/list/%s", lid);
1005+                xs *name = list_maint(&snac, lid, 3);
1006+                xs *title = xs_fmt(L("Showing timeline for list '%s'"), name);
1007+
1008+                *body = html_timeline(&snac, list, 0, skip, show,
1009+                    xs_list_len(next), title, base, 1);
1010+                *b_size = strlen(*body);
1011+                status  = 200;
1012+            }
1013+        }
1014+    }
1015+    else
1016     if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
1017         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
1018             return 403;
1019@@ -2640,7 +2803,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
1020     else
1021     if (xs_startswith(p_path, "s/")) { /** a static file **/
1022         xs *l    = xs_split(p_path, "/");
1023-        char *id = xs_list_get(l, 1);
1024+        const char *id = xs_list_get(l, 1);
1025         int sz;
1026 
1027         if (id && *id) {
1028@@ -2661,8 +2824,8 @@ int html_get_handler(const xs_dict *req, const char *q_path,
1029         if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
1030             return 403;
1031 
1032-        xs *l    = xs_split(p_path, "/");
1033-        char *id = xs_list_get(l, 1);
1034+        xs *l = xs_split(p_path, "/");
1035+        const char *id = xs_list_get(l, 1);
1036 
1037         if (id && *id) {
1038             if (xs_endswith(id, "timeline.html_")) {
1039@@ -2689,55 +2852,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
1040             xs_dict_get(srv_config, "host"));
1041         xs *rss_link = xs_fmt("%s.rss", snac.actor);
1042 
1043-        xs_html *rss = xs_html_tag("rss",
1044-            xs_html_attr("version", "0.91"));
1045-
1046-        xs_html *channel = xs_html_tag("channel",
1047-            xs_html_tag("title",
1048-                xs_html_text(rss_title)),
1049-            xs_html_tag("language",
1050-                xs_html_text("en")),
1051-            xs_html_tag("link",
1052-                xs_html_text(rss_link)),
1053-            xs_html_tag("description",
1054-                xs_html_text(bio)));
1055-
1056-        xs_html_add(rss, channel);
1057-
1058-        xs_list *p = elems;
1059-        char *v;
1060-
1061-        while (xs_list_iter(&p, &v)) {
1062-            xs *msg  = NULL;
1063-
1064-            if (!valid_status(timeline_get_by_md5(&snac, v, &msg)))
1065-                continue;
1066-
1067-            char *id = xs_dict_get(msg, "id");
1068-            char *content = xs_dict_get(msg, "content");
1069-
1070-            if (!xs_startswith(id, snac.actor))
1071-                continue;
1072-
1073-            /* create a title with the first line of the content */
1074-            xs *es_title = xs_replace(content, "<br>", "\n");
1075-            xs *title   = xs_str_new(NULL);
1076-            int i;
1077-
1078-            for (i = 0; es_title[i] && es_title[i] != '\n' && es_title[i] != '&' && i < 50; i++)
1079-                title = xs_append_m(title, &es_title[i], 1);
1080-
1081-            xs_html_add(channel,
1082-                xs_html_tag("item",
1083-                    xs_html_tag("title",
1084-                        xs_html_text(title)),
1085-                    xs_html_tag("link",
1086-                        xs_html_text(id)),
1087-                    xs_html_tag("description",
1088-                        xs_html_text(content))));
1089-        }
1090-
1091-        *body   = xs_html_render_s(rss, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
1092+        *body   = timeline_to_rss(&snac, elems, rss_title, rss_link, bio);
1093         *b_size = strlen(*body);
1094         *ctype  = "application/rss+xml; charset=utf-8";
1095         status  = 200;
1096@@ -2766,8 +2881,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1097 
1098     int status = 0;
1099     snac snac;
1100-    char *uid, *p_path;
1101-    xs_dict *p_vars;
1102+    const char *uid;
1103+    const char *p_path;
1104+    const xs_dict *p_vars;
1105 
1106     xs *l = xs_split_n(q_path, "/", 2);
1107 
1108@@ -2795,15 +2911,15 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1109 
1110     if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
1111         /* post note */
1112-        xs_str *content      = xs_dict_get(p_vars, "content");
1113-        xs_str *in_reply_to  = xs_dict_get(p_vars, "in_reply_to");
1114-        xs_str *attach_url   = xs_dict_get(p_vars, "attach_url");
1115-        xs_list *attach_file = xs_dict_get(p_vars, "attach");
1116-        xs_str *to           = xs_dict_get(p_vars, "to");
1117-        xs_str *sensitive    = xs_dict_get(p_vars, "sensitive");
1118-        xs_str *summary      = xs_dict_get(p_vars, "summary");
1119-        xs_str *edit_id      = xs_dict_get(p_vars, "edit_id");
1120-        xs_str *alt_text     = xs_dict_get(p_vars, "alt_text");
1121+        const xs_str *content      = xs_dict_get(p_vars, "content");
1122+        const xs_str *in_reply_to  = xs_dict_get(p_vars, "in_reply_to");
1123+        const xs_str *attach_url   = xs_dict_get(p_vars, "attach_url");
1124+        const xs_list *attach_file = xs_dict_get(p_vars, "attach");
1125+        const xs_str *to           = xs_dict_get(p_vars, "to");
1126+        const xs_str *sensitive    = xs_dict_get(p_vars, "sensitive");
1127+        const xs_str *summary      = xs_dict_get(p_vars, "summary");
1128+        const xs_str *edit_id      = xs_dict_get(p_vars, "edit_id");
1129+        const xs_str *alt_text     = xs_dict_get(p_vars, "alt_text");
1130         int priv             = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
1131         xs *attach_list      = xs_list_new();
1132 
1133@@ -2823,7 +2939,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1134 
1135         /* is attach_file set? */
1136         if (!xs_is_null(attach_file) && xs_type(attach_file) == XSTYPE_LIST) {
1137-            char *fn = xs_list_get(attach_file, 0);
1138+            const char *fn = xs_list_get(attach_file, 0);
1139 
1140             if (*fn != '\0') {
1141                 char *ext = strrchr(fn, '.');
1142@@ -2899,7 +3015,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1143                     int n;
1144 
1145                     for (n = 0; fields[n]; n++) {
1146-                        char *v = xs_dict_get(p_msg, fields[n]);
1147+                        const char *v = xs_dict_get(p_msg, fields[n]);
1148                         msg = xs_dict_set(msg, fields[n], v);
1149                     }
1150 
1151@@ -2928,10 +3044,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1152     else
1153     if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
1154         /* action on an entry */
1155-        char *id     = xs_dict_get(p_vars, "id");
1156-        char *actor  = xs_dict_get(p_vars, "actor");
1157-        char *action = xs_dict_get(p_vars, "action");
1158-        char *group  = xs_dict_get(p_vars, "group");
1159+        const char *id     = xs_dict_get(p_vars, "id");
1160+        const char *actor  = xs_dict_get(p_vars, "actor");
1161+        const char *action = xs_dict_get(p_vars, "action");
1162+        const char *group  = xs_dict_get(p_vars, "group");
1163 
1164         if (action == NULL)
1165             return 404;
1166@@ -3055,7 +3171,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1167         }
1168         else
1169         if (strcmp(action, L("Delete")) == 0) { /** **/
1170-            char *actor_form = xs_dict_get(p_vars, "actor-form");
1171+            const char *actor_form = xs_dict_get(p_vars, "actor-form");
1172             if (actor_form != NULL) {
1173                 /* delete follower */
1174                 if (valid_status(follower_del(&snac, actor)))
1175@@ -3099,8 +3215,8 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1176     else
1177     if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/
1178         /* change of user data */
1179-        char *v;
1180-        char *p1, *p2;
1181+        const char *v;
1182+        const char *p1, *p2;
1183 
1184         if ((v = xs_dict_get(p_vars, "name")) != NULL)
1185             snac.config = xs_dict_set(snac.config, "name", v);
1186@@ -3145,7 +3261,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1187             xs_dict *md = xs_dict_new();
1188             xs *l = xs_split(v, "\n");
1189             xs_list *p = l;
1190-            xs_str *kp;
1191+            const xs_str *kp;
1192 
1193             while (xs_list_iter(&p, &kp)) {
1194                 xs *kpl = xs_split_n(kp, "=", 1);
1195@@ -3166,7 +3282,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1196         for (n = 0; uploads[n]; n++) {
1197             xs *var_name = xs_fmt("%s_file", uploads[n]);
1198 
1199-            xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
1200+            const xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
1201             if (xs_type(uploaded_file) == XSTYPE_LIST) {
1202                 const char *fn = xs_list_get(uploaded_file, 0);
1203 
1204@@ -3231,7 +3347,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1205     }
1206     else
1207     if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
1208-        char *irt         = xs_dict_get(p_vars, "irt");
1209+        const char *irt   = xs_dict_get(p_vars, "irt");
1210         const char *opt   = xs_dict_get(p_vars, "question");
1211         const char *actor = xs_dict_get(p_vars, "actor");
1212 
1213@@ -3246,7 +3362,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1214         }
1215 
1216         xs_list *p = ls;
1217-        xs_str *v;
1218+        const xs_str *v;
1219 
1220         while (xs_list_iter(&p, &v)) {
1221             xs *msg = msg_note(&snac, "", actor, irt, NULL, 1);
1222@@ -3261,11 +3377,30 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1223             timeline_add(&snac, xs_dict_get(msg, "id"), msg);
1224         }
1225 
1226+        {
1227+            /* get the poll object */
1228+            xs *poll = NULL;
1229+
1230+            if (valid_status(object_get(irt, &poll))) {
1231+                const char *date = xs_dict_get(poll, "endTime");
1232+                if (xs_is_null(date))
1233+                    date = xs_dict_get(poll, "closed");
1234+
1235+                if (!xs_is_null(date)) {
1236+                    time_t t = xs_parse_iso_date(date, 0) - time(NULL);
1237+
1238+                    /* request the poll when it's closed;
1239+                       Pleroma does not send and update when the poll closes */
1240+                    enqueue_object_request(&snac, irt, t + 2);
1241+                }
1242+            }
1243+        }
1244+
1245         status = 303;
1246     }
1247 
1248     if (status == 303) {
1249-        char *redir = xs_dict_get(p_vars, "redir");
1250+        const char *redir = xs_dict_get(p_vars, "redir");
1251 
1252         if (xs_is_null(redir))
1253             redir = "top";
1254@@ -3278,3 +3413,71 @@ int html_post_handler(const xs_dict *req, const char *q_path,
1255 
1256     return status;
1257 }
1258+
1259+
1260+xs_str *timeline_to_rss(snac *user, const xs_list *timeline, char *title, char *link, char *desc)
1261+/* converts a timeline to rss */
1262+{
1263+    xs_html *rss = xs_html_tag("rss",
1264+        xs_html_attr("version", "0.91"));
1265+
1266+    xs_html *channel = xs_html_tag("channel",
1267+        xs_html_tag("title",
1268+            xs_html_text(title)),
1269+        xs_html_tag("language",
1270+            xs_html_text("en")),
1271+        xs_html_tag("link",
1272+            xs_html_text(link)),
1273+        xs_html_tag("description",
1274+            xs_html_text(desc)));
1275+
1276+    xs_html_add(rss, channel);
1277+
1278+    int c = 0;
1279+    const char *v;
1280+
1281+    while (xs_list_next(timeline, &v, &c)) {
1282+        xs *msg = NULL;
1283+
1284+        if (user) {
1285+            if (!valid_status(timeline_get_by_md5(user, v, &msg)))
1286+                continue;
1287+        }
1288+        else {
1289+            if (!valid_status(object_get_by_md5(v, &msg)))
1290+                continue;
1291+        }
1292+
1293+        const char *id = xs_dict_get(msg, "id");
1294+        const char *content = xs_dict_get(msg, "content");
1295+
1296+        if (user && !xs_startswith(id, user->actor))
1297+            continue;
1298+
1299+        /* create a title with the first line of the content */
1300+        xs *title = xs_replace(content, "<br>", "\n");
1301+        title = xs_regex_replace_i(title, "<[^>]+>", " ");
1302+        title = xs_regex_replace_i(title, "&[^;]+;", " ");
1303+        int i;
1304+
1305+        for (i = 0; title[i] && title[i] != '\n' && i < 50; i++);
1306+
1307+        if (title[i] != '\0') {
1308+            title[i] = '\0';
1309+            title = xs_str_cat(title, "...");
1310+        }
1311+
1312+        title = xs_strip_i(title);
1313+
1314+        xs_html_add(channel,
1315+            xs_html_tag("item",
1316+                xs_html_tag("title",
1317+                    xs_html_text(title)),
1318+                xs_html_tag("link",
1319+                    xs_html_text(id)),
1320+                xs_html_tag("description",
1321+                    xs_html_text(content))));
1322+    }
1323+
1324+    return xs_html_render_s(rss, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
1325+}
M http.c
+19, -17
  1@@ -12,7 +12,7 @@
  2 
  3 xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
  4                             const char *method, const char *url,
  5-                            xs_dict *headers,
  6+                            const xs_dict *headers,
  7                             const char *body, int b_size,
  8                             int *status, xs_str **payload, int *p_size,
  9                             int timeout)
 10@@ -24,15 +24,16 @@ xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
 11     xs *s64 = NULL;
 12     xs *signature = NULL;
 13     xs *hdrs = NULL;
 14-    char *host;
 15-    char *target;
 16-    char *k, *v;
 17+    const char *host;
 18+    const char *target;
 19+    const char *k, *v;
 20     xs_dict *response;
 21 
 22     date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT");
 23 
 24     {
 25-        xs *s = xs_replace_n(url, "https:/" "/", "", 1);
 26+        xs *s1 = xs_replace_n(url, "http:/" "/", "", 1);
 27+        xs *s = xs_replace_n(s1, "https:/" "/", "", 1);
 28         l1 = xs_split_n(s, "/", 1);
 29     }
 30 
 31@@ -105,13 +106,13 @@ xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
 32 
 33 
 34 xs_dict *http_signed_request(snac *snac, const char *method, const char *url,
 35-                            xs_dict *headers,
 36+                            const xs_dict *headers,
 37                             const char *body, int b_size,
 38                             int *status, xs_str **payload, int *p_size,
 39                             int timeout)
 40 /* does a signed HTTP request */
 41 {
 42-    char *seckey = xs_dict_get(snac->key, "secret");
 43+    const char *seckey = xs_dict_get(snac->key, "secret");
 44     xs_dict *response;
 45 
 46     response = http_signed_request_raw(snac->actor, seckey, method, url,
 47@@ -121,17 +122,18 @@ xs_dict *http_signed_request(snac *snac, const char *method, const char *url,
 48 }
 49 
 50 
 51-int check_signature(xs_dict *req, xs_str **err)
 52+int check_signature(const xs_dict *req, xs_str **err)
 53 /* check the signature */
 54 {
 55-    char *sig_hdr = xs_dict_get(req, "signature");
 56+    const char *sig_hdr = xs_dict_get(req, "signature");
 57     xs *keyId = NULL;
 58     xs *headers = NULL;
 59     xs *signature = NULL;
 60     xs *created = NULL;
 61     xs *expires = NULL;
 62-    char *pubkey;
 63     char *p;
 64+    const char *pubkey;
 65+    const char *k;
 66 
 67     if (xs_is_null(sig_hdr)) {
 68         *err = xs_fmt("missing 'signature' header");
 69@@ -141,10 +143,10 @@ int check_signature(xs_dict *req, xs_str **err)
 70     {
 71         /* extract the values */
 72         xs *l = xs_split(sig_hdr, ",");
 73-        xs_list *p = l;
 74-        xs_val *v;
 75+        int c = 0;
 76+        const xs_val *v;
 77 
 78-        while (xs_list_iter(&p, &v)) {
 79+        while (xs_list_next(l, &v, &c)) {
 80             xs *kv = xs_split_n(v, "=", 1);
 81 
 82             if (xs_list_len(kv) != 2)
 83@@ -191,8 +193,8 @@ int check_signature(xs_dict *req, xs_str **err)
 84         return 0;
 85     }
 86 
 87-    if ((p = xs_dict_get(actor, "publicKey")) == NULL ||
 88-        ((pubkey = xs_dict_get(p, "publicKeyPem")) == NULL)) {
 89+    if ((k = xs_dict_get(actor, "publicKey")) == NULL ||
 90+        ((pubkey = xs_dict_get(k, "publicKeyPem")) == NULL)) {
 91         *err = xs_fmt("cannot get pubkey from %s", keyId);
 92         return 0;
 93     }
 94@@ -203,11 +205,11 @@ int check_signature(xs_dict *req, xs_str **err)
 95     {
 96         xs *l = xs_split(headers, " ");
 97         xs_list *p;
 98-        xs_val *v;
 99+        const xs_val *v;
100 
101         p = l;
102         while (xs_list_iter(&p, &v)) {
103-            char *hc;
104+            const char *hc;
105             xs *ss = NULL;
106 
107             if (*sig_str != '\0')
+50, -16
  1@@ -75,7 +75,7 @@ xs_str *nodeinfo_2_0(void)
  2     int n_posts  = 0;
  3     xs *users = user_list();
  4     xs_list *p = users;
  5-    char *v;
  6+    const char *v;
  7     double now = (double)time(NULL);
  8 
  9     while (xs_list_iter(&p, &v)) {
 10@@ -125,10 +125,10 @@ static xs_str *greeting_html(void)
 11 
 12         /* does it have a %userlist% mark? */
 13         if (xs_str_in(s, "%userlist%") != -1) {
 14-            char *host = xs_dict_get(srv_config, "host");
 15+            const char *host = xs_dict_get(srv_config, "host");
 16             xs *list = user_list();
 17             xs_list *p = list;
 18-            xs_str *uid;
 19+            const xs_str *uid;
 20 
 21             xs_html *ul = xs_html_tag("ul",
 22                 xs_html_attr("class", "snac-user-list"));
 23@@ -169,18 +169,16 @@ int server_get_handler(xs_dict *req, const char *q_path,
 24 {
 25     int status = 0;
 26 
 27-    (void)req;
 28-
 29     /* is it the server root? */
 30     if (*q_path == '\0') {
 31-        xs_dict *q_vars = xs_dict_get(req, "q_vars");
 32-        char *t = NULL;
 33+        const xs_dict *q_vars = xs_dict_get(req, "q_vars");
 34+        const char *t = NULL;
 35 
 36         if (xs_type(q_vars) == XSTYPE_DICT && (t = xs_dict_get(q_vars, "t"))) {
 37             /** search by tag **/
 38             int skip = 0;
 39             int show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries"));
 40-            char *v;
 41+            const char *v;
 42 
 43             if ((v = xs_dict_get(q_vars, "skip")) != NULL)
 44                 skip = atoi(v);
 45@@ -195,13 +193,25 @@ int server_get_handler(xs_dict *req, const char *q_path,
 46                 more = 1;
 47             }
 48 
 49-            *body = html_timeline(NULL, tl, 0, skip, show, more, t, NULL, 0);
 50+            const char *accept = xs_dict_get(req, "accept");
 51+            if (!xs_is_null(accept) && strcmp(accept, "application/rss+xml") == 0) {
 52+                xs *link = xs_fmt("%s/?t=%s", srv_baseurl, t);
 53+
 54+                *body = timeline_to_rss(NULL, tl, link, link, link);
 55+                *ctype = "application/rss+xml; charset=utf-8";
 56+            }
 57+            else {
 58+                xs *page = xs_fmt("?t=%s", t);
 59+                xs *title = xs_fmt(L("Search results for tag #%s"), t);
 60+                *body = html_timeline(NULL, tl, 0, skip, show, more, title, page, 0);
 61+            }
 62         }
 63         else
 64         if (xs_type(xs_dict_get(srv_config, "show_instance_timeline")) == XSTYPE_TRUE) {
 65             /** instance timeline **/
 66             xs *tl = timeline_instance_list(0, 30);
 67-            *body = html_timeline(NULL, tl, 0, 0, 0, 0, NULL, NULL, 0);
 68+            *body = html_timeline(NULL, tl, 0, 0, 0, 0,
 69+                L("Recent posts by users in this instance"), NULL, 0);
 70         }
 71         else
 72             *body = greeting_html();
 73@@ -258,7 +268,7 @@ void httpd_connection(FILE *f)
 74 /* the connection processor */
 75 {
 76     xs *req;
 77-    char *method;
 78+    const char *method;
 79     int status   = 0;
 80     xs_str *body = NULL;
 81     int b_size   = 0;
 82@@ -268,7 +278,7 @@ void httpd_connection(FILE *f)
 83     xs *payload  = NULL;
 84     xs *etag     = NULL;
 85     int p_size   = 0;
 86-    char *p;
 87+    const char *p;
 88     int fcgi_id;
 89 
 90     if (p_state->use_fcgi)
 91@@ -360,7 +370,7 @@ void httpd_connection(FILE *f)
 92 #ifndef NO_MASTODON_API
 93         if (status == 0)
 94             status = mastoapi_delete_handler(req, q_path,
 95-                    &body, &b_size, &ctype);
 96+                    payload, p_size, &body, &b_size, &ctype);
 97 #endif
 98     }
 99 
100@@ -401,9 +411,9 @@ void httpd_connection(FILE *f)
101         headers = xs_dict_append(headers, "etag", etag);
102 
103     /* if there are any additional headers, add them */
104-    xs_dict *more_headers = xs_dict_get(srv_config, "http_headers");
105+    const xs_dict *more_headers = xs_dict_get(srv_config, "http_headers");
106     if (xs_type(more_headers) == XSTYPE_DICT) {
107-        char *k, *v;
108+        const char *k, *v;
109         int c = 0;
110         while (xs_dict_next(more_headers, &k, &v, &c))
111             headers = xs_dict_set(headers, k, v);
112@@ -580,7 +590,8 @@ static void *background_thread(void *arg)
113 
114         {
115             xs *list = user_list();
116-            char *p, *uid;
117+            char *p;
118+            const char *uid;
119 
120             /* process queues for all users */
121             p = list;
122@@ -654,6 +665,13 @@ srv_state *srv_state_op(xs_str **fname, int op)
123 
124     switch (op) {
125     case 0: /* open for writing */
126+
127+#ifdef WITHOUT_SHM
128+
129+        errno = ENOTSUP;
130+
131+#else
132+
133         if ((fd = shm_open(*fname, O_CREAT | O_RDWR, 0666)) != -1) {
134             ftruncate(fd, sizeof(*ss));
135 
136@@ -664,6 +682,8 @@ srv_state *srv_state_op(xs_str **fname, int op)
137             close(fd);
138         }
139 
140+#endif
141+
142         if (ss == NULL) {
143             /* shared memory error: just create a plain structure */
144             srv_log(xs_fmt("warning: shm object error (%s)", strerror(errno)));
145@@ -677,6 +697,13 @@ srv_state *srv_state_op(xs_str **fname, int op)
146         break;
147 
148     case 1: /* open for reading */
149+
150+#ifdef WITHOUT_SHM
151+
152+        errno = ENOTSUP;
153+
154+#else
155+
156         if ((fd = shm_open(*fname, O_RDONLY, 0666)) != -1) {
157             if ((ss = mmap(0, sizeof(*ss), PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED)
158                 ss = NULL;
159@@ -684,6 +711,8 @@ srv_state *srv_state_op(xs_str **fname, int op)
160             close(fd);
161         }
162 
163+#endif
164+
165         if (ss == NULL) {
166             /* shared memory error */
167             srv_log(xs_fmt("error: shm object error (%s) server not running?", strerror(errno)));
168@@ -701,9 +730,14 @@ srv_state *srv_state_op(xs_str **fname, int op)
169         break;
170 
171     case 2: /* unlink */
172+
173+#ifndef WITHOUT_SHM
174+
175         if (*fname)
176             shm_unlink(*fname);
177 
178+#endif
179+
180         break;
181     }
182 
M main.c
+25, -1
 1@@ -44,6 +44,7 @@ int usage(void)
 2     printf("limit {basedir} {uid} {actor}        Limits an actor (drops their announces)\n");
 3     printf("unlimit {basedir} {uid} {actor}      Unlimits an actor\n");
 4     printf("verify_links {basedir} {uid}         Verifies a user's links (in the metadata)\n");
 5+    printf("search {basedir} {uid} {regex}       Searches posts by content\n");
 6 
 7     return 1;
 8 }
 9@@ -314,7 +315,7 @@ int main(int argc, char *argv[])
10         xs *msg = msg_follow(&snac, url);
11 
12         if (msg != NULL) {
13-            char *actor = xs_dict_get(msg, "object");
14+            const char *actor = xs_dict_get(msg, "object");
15 
16             following_add(&snac, actor, msg);
17 
18@@ -374,6 +375,23 @@ int main(int argc, char *argv[])
19         return 0;
20     }
21 
22+    if (strcmp(cmd, "search") == 0) { /** **/
23+        int to;
24+
25+        /* 'url' contains the regex */
26+        xs *r = content_search(&snac, url, 1, 0, XS_ALL, 10, &to);
27+
28+        int c = 0;
29+        const char *v;
30+
31+        /* print results as standalone links */
32+        while (xs_list_next(r, &v, &c)) {
33+            printf("%s/admin/p/%s\n", snac.actor, v);
34+        }
35+
36+        return 0;
37+    }
38+
39     if (strcmp(cmd, "ping") == 0) { /** **/
40         xs *actor_o = NULL;
41 
42@@ -458,6 +476,12 @@ int main(int argc, char *argv[])
43         return 0;
44     }
45 
46+    if (strcmp(cmd, "request2") == 0) { /** **/
47+        enqueue_object_request(&snac, url, 2);
48+
49+        return 0;
50+    }
51+
52     if (strcmp(cmd, "actor") == 0) { /** **/
53         int status;
54         xs *data = NULL;
+404, -91
  1@@ -156,7 +156,7 @@ const char *login_page = ""
  2 "</head>\n"
  3 "<body><h1>%s OAuth identify</h1>\n"
  4 "<div style=\"background-color: red; color: white\">%s</div>\n"
  5-"<form method=\"post\" action=\"https:/" "/%s/%s\">\n"
  6+"<form method=\"post\" action=\"%s:/" "/%s/%s\">\n"
  7 "<p>Login: <input type=\"text\" name=\"login\"></p>\n"
  8 "<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
  9 "<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
 10@@ -175,7 +175,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
 11         return 0;
 12 
 13     int status   = 404;
 14-    xs_dict *msg = xs_dict_get(req, "q_vars");
 15+    const xs_dict *msg = xs_dict_get(req, "q_vars");
 16     xs *cmd      = xs_replace_n(q_path, "/oauth", "", 1);
 17 
 18     srv_debug(1, xs_fmt("oauth_get_handler %s", q_path));
 19@@ -193,11 +193,12 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
 20 
 21             if (app != NULL) {
 22                 const char *host = xs_dict_get(srv_config, "host");
 23+                const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 24 
 25                 if (xs_is_null(state))
 26                     state = "";
 27 
 28-                *body  = xs_fmt(login_page, host, host, "", host, "oauth/x-snac-login",
 29+                *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-login",
 30                                 ruri, cid, state, USER_AGENT);
 31                 *ctype = "text/html";
 32                 status = 200;
 33@@ -213,8 +214,9 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
 34     else
 35     if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
 36         const char *host = xs_dict_get(srv_config, "host");
 37+        const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 38 
 39-        *body  = xs_fmt(login_page, host, host, "", host, "oauth/x-snac-get-token",
 40+        *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-get-token",
 41                         "", "", "", USER_AGENT);
 42         *ctype = "text/html";
 43         status = 200;
 44@@ -237,7 +239,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 45 
 46     int status   = 404;
 47 
 48-    char *i_ctype = xs_dict_get(req, "content-type");
 49+    const char *i_ctype = xs_dict_get(req, "content-type");
 50     xs *args      = NULL;
 51 
 52     if (i_ctype && xs_startswith(i_ctype, "application/json")) {
 53@@ -265,11 +267,11 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 54         const char *redir  = xs_dict_get(args, "redir");
 55         const char *cid    = xs_dict_get(args, "cid");
 56         const char *state  = xs_dict_get(args, "state");
 57-
 58-        const char *host = xs_dict_get(srv_config, "host");
 59+        const char *host   = xs_dict_get(srv_config, "host");
 60+        const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 61 
 62         /* by default, generate another login form with an error */
 63-        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", host, "oauth/x-snac-login",
 64+        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-login",
 65                         redir, cid, state, USER_AGENT);
 66         *ctype = "text/html";
 67         status = 200;
 68@@ -289,7 +291,11 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 69                         *body = xs_dup(code);
 70                     }
 71                     else {
 72-                        *body = xs_fmt("%s?code=%s", redir, code);
 73+                        if (xs_str_in(redir, "?") != -1)
 74+                            *body = xs_fmt("%s&code=%s", redir, code);
 75+                        else
 76+                            *body = xs_fmt("%s?code=%s", redir, code);
 77+
 78                         status = 303;
 79                     }
 80 
 81@@ -446,11 +452,11 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 82     if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
 83         const char *login  = xs_dict_get(args, "login");
 84         const char *passwd = xs_dict_get(args, "passwd");
 85-
 86-        const char *host = xs_dict_get(srv_config, "host");
 87+        const char *host   = xs_dict_get(srv_config, "host");
 88+        const char *proto  = xs_dict_get_def(srv_config, "protocol", "https");
 89 
 90         /* by default, generate another login form with an error */
 91-        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", host, "oauth/x-snac-get-token",
 92+        *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-get-token",
 93                         "", "", "", USER_AGENT);
 94         *ctype = "text/html";
 95         status = 200;
 96@@ -544,6 +550,9 @@ xs_dict *mastoapi_account(const xs_dict *actor)
 97         acct = xs_dict_append(acct, "created_at", date);
 98     }
 99 
100+    xs *last_status_at = xs_str_utctime(0, "%Y-%m-%d");
101+    acct = xs_dict_append(acct, "last_status_at", last_status_at);
102+
103     const char *note = xs_dict_get(actor, "summary");
104     if (xs_is_null(note))
105         note = "";
106@@ -559,10 +568,10 @@ xs_dict *mastoapi_account(const xs_dict *actor)
107     acct = xs_dict_append(acct, "uri", id);
108 
109     xs *avatar  = NULL;
110-    xs_dict *av = xs_dict_get(actor, "icon");
111+    const xs_dict *av = xs_dict_get(actor, "icon");
112 
113     if (xs_type(av) == XSTYPE_DICT) {
114-        char *url = xs_dict_get(av, "url");
115+        const char *url = xs_dict_get(av, "url");
116 
117         if (url != NULL)
118             avatar = xs_dup(url);
119@@ -575,7 +584,7 @@ xs_dict *mastoapi_account(const xs_dict *actor)
120     acct = xs_dict_append(acct, "avatar_static", avatar);
121 
122     xs *header  = NULL;
123-    xs_dict *hd = xs_dict_get(actor, "image");
124+    const xs_dict *hd = xs_dict_get(actor, "image");
125 
126     if (xs_type(hd) == XSTYPE_DICT)
127         header = xs_dup(xs_dict_get(hd, "url"));
128@@ -587,12 +596,13 @@ xs_dict *mastoapi_account(const xs_dict *actor)
129     acct = xs_dict_append(acct, "header_static", header);
130 
131     /* emojis */
132-    xs_list *p;
133+    const xs_list *p;
134     if (!xs_is_null(p = xs_dict_get(actor, "tag"))) {
135         xs *eml = xs_list_new();
136-        xs_dict *v;
137+        const xs_dict *v;
138+        int c = 0;
139 
140-        while (xs_list_iter(&p, &v)) {
141+        while (xs_list_next(p, &v, &c)) {
142             const char *type = xs_dict_get(v, "type");
143 
144             if (!xs_is_null(type) && strcmp(type, "Emoji") == 0) {
145@@ -627,11 +637,11 @@ xs_dict *mastoapi_account(const xs_dict *actor)
146 
147     xs *fields = xs_list_new();
148     p = xs_dict_get(actor, "attachment");
149-    xs_dict *v;
150+    const xs_dict *v;
151 
152     /* dict of validated links */
153     xs_dict *val_links = NULL;
154-    xs_dict *metadata  = xs_stock(XSTYPE_DICT);
155+    const xs_dict *metadata  = xs_stock(XSTYPE_DICT);
156     snac user = {0};
157 
158     if (xs_startswith(id, srv_baseurl)) {
159@@ -645,19 +655,20 @@ xs_dict *mastoapi_account(const xs_dict *actor)
160     if (xs_is_null(val_links))
161         val_links = xs_stock(XSTYPE_DICT);
162 
163-    while (xs_list_iter(&p, &v)) {
164-        char *type  = xs_dict_get(v, "type");
165-        char *name  = xs_dict_get(v, "name");
166-        char *value = xs_dict_get(v, "value");
167+    int c = 0;
168+    while (xs_list_next(p, &v, &c)) {
169+        const char *type  = xs_dict_get(v, "type");
170+        const char *name  = xs_dict_get(v, "name");
171+        const char *value = xs_dict_get(v, "value");
172 
173         if (!xs_is_null(type) && !xs_is_null(name) &&
174             !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) {
175             xs *val_date = NULL;
176 
177-            char *url = xs_dict_get(metadata, name);
178+            const char *url = xs_dict_get(metadata, name);
179 
180             if (!xs_is_null(url) && xs_startswith(url, "https:/" "/")) {
181-                xs_number *verified_time = xs_dict_get(val_links, url);
182+                const xs_number *verified_time = xs_dict_get(val_links, url);
183                 if (xs_type(verified_time) == XSTYPE_NUMBER) {
184                     time_t t = xs_number_get(verified_time);
185 
186@@ -686,7 +697,7 @@ xs_dict *mastoapi_account(const xs_dict *actor)
187 }
188 
189 
190-xs_str *mastoapi_date(char *date)
191+xs_str *mastoapi_date(const char *date)
192 /* converts an ISO 8601 date to whatever format Mastodon uses */
193 {
194     xs_str *s = xs_crop_i(xs_dup(date), 0, 19);
195@@ -701,16 +712,29 @@ xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg)
196 {
197     xs_dict *poll = xs_dict_new();
198     xs *mid       = mastoapi_id(msg);
199-    xs_list *opts = NULL;
200-    xs_val *v;
201+    const xs_list *opts = NULL;
202+    const xs_val *v;
203     int num_votes = 0;
204     xs *options = xs_list_new();
205 
206     poll = xs_dict_append(poll, "id", mid);
207-    xs *fd = mastoapi_date(xs_dict_get(msg, "endTime"));
208+    const char *date = xs_dict_get(msg, "endTime");
209+    if (date == NULL)
210+        date = xs_dict_get(msg, "closed");
211+    if (date == NULL)
212+        return NULL;
213+
214+    xs *fd = mastoapi_date(date);
215     poll = xs_dict_append(poll, "expires_at", fd);
216+
217+    date = xs_dict_get(msg, "closed");
218+    time_t t = 0;
219+
220+    if (date != NULL)
221+        t = xs_parse_iso_date(date, 0);
222+
223     poll = xs_dict_append(poll, "expired",
224-            xs_dict_get(msg, "closed") != NULL ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
225+            t < time(NULL) ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE));
226 
227     if ((opts = xs_dict_get(msg, "oneOf")) != NULL)
228         poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_FALSE));
229@@ -719,7 +743,8 @@ xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg)
230         poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_TRUE));
231     }
232 
233-    while (xs_list_iter(&opts, &v)) {
234+    int c = 0;
235+    while (xs_list_next(opts, &v, &c)) {
236         const char *title   = xs_dict_get(v, "name");
237         const char *replies = xs_dict_get(v, "replies");
238 
239@@ -772,7 +797,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
240 
241     xs *idx = NULL;
242     xs *ixc = NULL;
243-    char *tmp;
244+    const char *tmp;
245     xs *mid  = mastoapi_id(msg);
246 
247     xs_dict *st = xs_dict_new();
248@@ -824,14 +849,14 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
249 
250     {
251         xs_list *p = attach;
252-        xs_dict *v;
253+        const xs_dict *v;
254 
255         xs *matt = xs_list_new();
256 
257         while (xs_list_iter(&p, &v)) {
258-            char *type = xs_dict_get(v, "type");
259-            char *href = xs_dict_get(v, "href");
260-            char *name = xs_dict_get(v, "name");
261+            const char *type = xs_dict_get(v, "type");
262+            const char *href = xs_dict_get(v, "href");
263+            const char *name = xs_dict_get(v, "name");
264 
265             if (xs_match(type, "image/*|video/*|Image|Video")) { /* */
266                 xs *matteid = xs_fmt("%s_%d", id, xs_list_len(matt));
267@@ -857,7 +882,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
268         xs *ml  = xs_list_new();
269         xs *htl = xs_list_new();
270         xs *eml = xs_list_new();
271-        xs_list *tag = xs_dict_get(msg, "tag");
272+        const xs_list *tag = xs_dict_get(msg, "tag");
273         int n = 0;
274 
275         xs *tag_list = NULL;
276@@ -873,9 +898,10 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
277             tag_list = xs_list_new();
278 
279         tag = tag_list;
280-        xs_dict *v;
281+        const xs_dict *v;
282 
283-        while (xs_list_iter(&tag, &v)) {
284+        int c = 0;
285+        while (xs_list_next(tag, &v, &c)) {
286             const char *type = xs_dict_get(v, "type");
287 
288             if (xs_is_null(type))
289@@ -984,7 +1010,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
290             xs *irt_mid = mastoapi_id(irto);
291             st = xs_dict_set(st, "in_reply_to_id", irt_mid);
292 
293-            char *at = NULL;
294+            const char *at = NULL;
295             if (!xs_is_null(at = get_atto(irto))) {
296                 xs *at_md5 = xs_md5_hex(at, strlen(at));
297                 st = xs_dict_set(st, "in_reply_to_account_id", at_md5);
298@@ -994,7 +1020,10 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
299 
300     st = xs_dict_append(st, "reblog",   xs_stock(XSTYPE_NULL));
301     st = xs_dict_append(st, "card",     xs_stock(XSTYPE_NULL));
302-    st = xs_dict_append(st, "language", xs_stock(XSTYPE_NULL));
303+    st = xs_dict_append(st, "language", "en");
304+
305+    st = xs_dict_append(st, "filtered", xs_stock(XSTYPE_LIST));
306+    st = xs_dict_append(st, "muted",    xs_stock(XSTYPE_FALSE));
307 
308     tmp = xs_dict_get(msg, "sourceContent");
309     if (xs_is_null(tmp))
310@@ -1093,7 +1122,7 @@ int process_auth_token(snac *snac, const xs_dict *req)
311 /* processes an authorization token, if there is one */
312 {
313     int logged_in = 0;
314-    char *v;
315+    const char *v;
316 
317     /* if there is an authorization field, try to validate it */
318     if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
319@@ -1131,7 +1160,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
320         return 0;
321 
322     int status    = 404;
323-    xs_dict *args = xs_dict_get(req, "q_vars");
324+    const xs_dict *args = xs_dict_get(req, "q_vars");
325     xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
326 
327     snac snac1 = {0};
328@@ -1157,7 +1186,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
329             acct = xs_dict_append(acct, "source", src);
330 
331             xs *avatar = NULL;
332-            char *av   = xs_dict_get(snac1.config, "avatar");
333+            const char *av   = xs_dict_get(snac1.config, "avatar");
334 
335             if (xs_is_null(av) || *av == '\0')
336                 avatar = xs_fmt("%s/susie.png", srv_baseurl);
337@@ -1168,7 +1197,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
338             acct = xs_dict_append(acct, "avatar_static", avatar);
339 
340             xs *header = NULL;
341-            char *hd = xs_dict_get(snac1.config, "header");
342+            const char *hd = xs_dict_get(snac1.config, "header");
343 
344             if (!xs_is_null(hd))
345                 header = xs_dup(hd);
346@@ -1178,11 +1207,11 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
347             acct = xs_dict_append(acct, "header",        header);
348             acct = xs_dict_append(acct, "header_static", header);
349 
350-            xs_dict *metadata = xs_dict_get(snac1.config, "metadata");
351+            const xs_dict *metadata = xs_dict_get(snac1.config, "metadata");
352             if (xs_type(metadata) == XSTYPE_DICT) {
353                 xs *fields = xs_list_new();
354-                xs_str *k;
355-                xs_str *v;
356+                const xs_str *k;
357+                const xs_str *v;
358 
359                 xs_dict *val_links = snac1.links;
360                 if (xs_is_null(val_links))
361@@ -1192,7 +1221,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
362                 while (xs_dict_next(metadata, &k, &v, &c)) {
363                     xs *val_date = NULL;
364 
365-                    xs_number *verified_time = xs_dict_get(val_links, v);
366+                    const xs_number *verified_time = xs_dict_get(val_links, v);
367                     if (xs_type(verified_time) == XSTYPE_NUMBER) {
368                         time_t t = xs_number_get(verified_time);
369 
370@@ -1258,13 +1287,13 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
371     else
372     if (strcmp(cmd, "/v1/accounts/lookup") == 0) { /** **/
373         /* lookup an account */
374-        char *acct = xs_dict_get(args, "acct");
375+        const char *acct = xs_dict_get(args, "acct");
376 
377         if (!xs_is_null(acct)) {
378             xs *s = xs_strip_chars_i(xs_dup(acct), "@");
379             xs *l = xs_split_n(s, "@", 1);
380-            char *uid = xs_list_get(l, 0);
381-            char *host = xs_list_get(l, 1);
382+            const char *uid = xs_list_get(l, 0);
383+            const char *host = xs_list_get(l, 1);
384 
385             if (uid && (!host || strcmp(host, xs_dict_get(srv_config, "host")) == 0)) {
386                 snac user;
387@@ -1305,7 +1334,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
388                     xs *wers = follower_list(&snac1);
389                     xs *ulst = user_list();
390                     xs_list *p;
391-                    xs_str *v;
392+                    const xs_str *v;
393                     xs_set seen;
394 
395                     xs_set_init(&seen);
396@@ -1381,7 +1410,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
397                     /* the public list of posts of a user */
398                     xs *timeline = timeline_simple_list(&snac2, "public", 0, 256);
399                     xs_list *p   = timeline;
400-                    xs_str *v;
401+                    const xs_str *v;
402 
403                     out = xs_list_new();
404 
405@@ -1446,7 +1475,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
406 
407             xs *out      = xs_list_new();
408             xs_list *p   = timeline;
409-            xs_str *v;
410+            const xs_str *v;
411 
412             while (xs_list_iter(&p, &v) && cnt < limit) {
413                 xs *msg = NULL;
414@@ -1479,7 +1508,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
415                 /* discard non-Notes */
416                 const char *id   = xs_dict_get(msg, "id");
417                 const char *type = xs_dict_get(msg, "type");
418-                if (!xs_match(type, "Note|Question|Page|Article|Video"))
419+                if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
420                     continue;
421 
422                 const char *from = NULL;
423@@ -1550,7 +1579,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
424         xs *timeline = timeline_instance_list(0, limit);
425         xs *out      = xs_list_new();
426         xs_list *p   = timeline;
427-        xs_str *md5;
428+        const xs_str *md5;
429 
430         snac *user = NULL;
431         if (logged_in)
432@@ -1599,12 +1628,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
433 
434         /* get the tag */
435         xs *l = xs_split(cmd, "/");
436-        char *tag = xs_list_get(l, -1);
437+        const char *tag = xs_list_get(l, -1);
438 
439         xs *timeline = tag_search(tag, 0, limit);
440         xs *out      = xs_list_new();
441         xs_list *p   = timeline;
442-        xs_str *md5;
443+        const xs_str *md5;
444 
445         while (xs_list_iter(&p, &md5) && cnt < limit) {
446             xs *msg = NULL;
447@@ -1635,6 +1664,77 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
448         status = 200;
449     }
450     else
451+    if (xs_startswith(cmd, "/v1/timelines/list/")) { /** **/
452+        /* get the list id */
453+        if (logged_in) {
454+            xs *l = xs_split(cmd, "/");
455+            const char *list = xs_list_get(l, -1);
456+
457+            xs *timeline = list_timeline(&snac1, list, 0, 2048);
458+            xs *out      = xs_list_new();
459+            int c = 0;
460+            const char *md5;
461+
462+            while (xs_list_next(timeline, &md5, &c)) {
463+                xs *msg = NULL;
464+
465+                /* get the entry */
466+                if (!valid_status(timeline_get_by_md5(&snac1, md5, &msg)))
467+                    continue;
468+
469+                /* discard non-Notes */
470+                const char *id   = xs_dict_get(msg, "id");
471+                const char *type = xs_dict_get(msg, "type");
472+                if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
473+                    continue;
474+
475+                const char *from = NULL;
476+                if (strcmp(type, "Page") == 0)
477+                    from = xs_dict_get(msg, "audience");
478+
479+                if (from == NULL)
480+                    from = get_atto(msg);
481+
482+                if (from == NULL)
483+                    continue;
484+
485+                /* is this message from a person we don't follow? */
486+                if (strcmp(from, snac1.actor) && !following_check(&snac1, from)) {
487+                    /* discard if it was not boosted */
488+                    xs *idx = object_announces(id);
489+
490+                    if (xs_list_len(idx) == 0)
491+                        continue;
492+                }
493+
494+                /* discard notes from muted morons */
495+                if (is_muted(&snac1, from))
496+                    continue;
497+
498+                /* discard hidden notes */
499+                if (is_hidden(&snac1, id))
500+                    continue;
501+
502+                /* if it has a name and it's not a Page or a Video,
503+                   it's a poll vote, so discard it */
504+                if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video"))
505+                    continue;
506+
507+                /* convert the Note into a Mastodon status */
508+                xs *st = mastoapi_status(&snac1, msg);
509+
510+                if (st != NULL)
511+                    out = xs_list_append(out, st);
512+            }
513+
514+            *body  = xs_json_dumps(out, 4);
515+            *ctype = "application/json";
516+            status = 200;
517+        }
518+        else
519+            status = 421;
520+    }
521+    else
522     if (strcmp(cmd, "/v1/conversations") == 0) { /** **/
523         /* TBD */
524         *body  = xs_dup("[]");
525@@ -1647,8 +1747,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
526             xs *l      = notify_list(&snac1, 0, 64);
527             xs *out    = xs_list_new();
528             xs_list *p = l;
529-            xs_dict *v;
530-            xs_list *excl = xs_dict_get(args, "exclude_types[]");
531+            const xs_dict *v;
532+            const xs_list *excl = xs_dict_get(args, "exclude_types[]");
533 
534             while (xs_list_iter(&p, &v)) {
535                 xs *noti = notify_get(&snac1, v);
536@@ -1753,11 +1853,88 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
537         status = 200;
538     }
539     else
540-    if (strcmp(cmd, "/v1/lists") == 0) { /** **/
541-        /* snac does not support lists */
542-        *body  = xs_dup("[]");
543-        *ctype = "application/json";
544-        status = 200;
545+    if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
546+        if (logged_in) {
547+            xs *lol = list_maint(&snac1, NULL, 0);
548+            xs *l   = xs_list_new();
549+            int c = 0;
550+            const xs_list *li;
551+
552+            while (xs_list_next(lol, &li, &c)) {
553+                xs *d = xs_dict_new();
554+
555+                d = xs_dict_append(d, "id", xs_list_get(li, 0));
556+                d = xs_dict_append(d, "title", xs_list_get(li, 1));
557+                d = xs_dict_append(d, "replies_policy", "list");
558+                d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
559+
560+                l = xs_list_append(l, d);
561+            }
562+
563+            *body  = xs_json_dumps(l, 4);
564+            *ctype = "application/json";
565+            status = 200;
566+        }
567+    }
568+    else
569+    if (xs_startswith(cmd, "/v1/lists/")) { /** list information **/
570+        if (logged_in) {
571+            xs *l = xs_split(cmd, "/");
572+            const char *p = xs_list_get(l, -1);
573+
574+            if (p) {
575+                if (strcmp(p, "accounts") == 0) {
576+                    p = xs_list_get(l, -2);
577+
578+                    if (p && xs_is_hex(p)) {
579+                        xs *actors = list_content(&snac1, p, NULL, 0);
580+                        xs *out = xs_list_new();
581+                        int c = 0;
582+                        const char *v;
583+
584+                        while (xs_list_next(actors, &v, &c)) {
585+                            xs *actor = NULL;
586+
587+                            if (valid_status(object_get_by_md5(v, &actor))) {
588+                                xs *acct = mastoapi_account(actor);
589+                                out = xs_list_append(out, acct);
590+                            }
591+                        }
592+
593+                        *body  = xs_json_dumps(out, 4);
594+                        *ctype = "application/json";
595+                        status = 200;
596+                    }
597+                }
598+                else
599+                if (xs_is_hex(p)) {
600+                    xs *out = xs_list_new();
601+                    xs *lol = list_maint(&snac1, NULL, 0);
602+                    int c = 0;
603+                    const xs_list *v;
604+
605+                    while (xs_list_next(lol, &v, &c)) {
606+                        const char *id = xs_list_get(v, 0);
607+
608+                        if (id && strcmp(id, p) == 0) {
609+                            xs *d = xs_dict_new();
610+
611+                            d = xs_dict_append(d, "id", p);
612+                            d = xs_dict_append(d, "title", xs_list_get(v, 1));
613+                            d = xs_dict_append(d, "replies_policy", "list");
614+                            d = xs_dict_append(d, "exclusive", xs_stock(XSTYPE_FALSE));
615+
616+                            out = xs_list_append(out, d);
617+                            break;
618+                        }
619+                    }
620+
621+                    *body  = xs_json_dumps(out, 4);
622+                    *ctype = "application/json";
623+                    status = 200;
624+                }
625+            }
626+        }
627     }
628     else
629     if (strcmp(cmd, "/v1/scheduled_statuses") == 0) { /** **/
630@@ -1928,7 +2105,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
631                         xs *anc = xs_list_new();
632                         xs *des = xs_list_new();
633                         xs_list *p;
634-                        xs_str *v;
635+                        const xs_str *v;
636                         char pid[64];
637 
638                         /* build the [grand]parent list, moving up */
639@@ -1982,7 +2159,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
640                             l = object_likes(xs_dict_get(msg, "id"));
641 
642                         xs_list *p = l;
643-                        xs_str *v;
644+                        const xs_str *v;
645 
646                         while (xs_list_iter(&p, &v)) {
647                             xs *actor2 = NULL;
648@@ -2052,8 +2229,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
649                 /* reply something only for offset 0; otherwise,
650                    apps like Tusky keep asking again and again */
651 
652-                if (!xs_is_null(q) && !xs_is_null(type)) {
653-                    if (strcmp(type, "accounts") == 0) {
654+                if (!xs_is_null(q)) {
655+                    if (xs_is_null(type) || strcmp(type, "accounts") == 0) {
656                         /* do a webfinger query */
657                         char *actor = NULL;
658                         char *user  = NULL;
659@@ -2068,8 +2245,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
660                             }
661                         }
662                     }
663-                    else
664-                    if (strcmp(type, "hashtags") == 0) {
665+
666+                    if (xs_is_null(type) || strcmp(type, "hashtags") == 0) {
667                         /* search this tag */
668                         xs *tl = tag_search((char *)q, 0, 1);
669 
670@@ -2084,6 +2261,26 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
671                             htl = xs_list_append(htl, d);
672                         }
673                     }
674+
675+                    if (xs_is_null(type) || strcmp(type, "statuses") == 0) {
676+                        int to = 0;
677+                        int cnt = 40;
678+                        xs *tl = content_search(&snac1, q, 1, 0, cnt, 0, &to);
679+                        int c = 0;
680+                        const char *v;
681+
682+                        while (xs_list_next(tl, &v, &c) && --cnt) {
683+                            xs *post = NULL;
684+
685+                            if (!valid_status(timeline_get_by_md5(&snac1, v, &post)))
686+                                continue;
687+
688+                            xs *s = mastoapi_status(&snac1, post);
689+
690+                            if (!xs_is_null(s))
691+                                stl = xs_list_append(stl, s);
692+                        }
693+                    }
694                 }
695             }
696 
697@@ -2119,11 +2316,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
698     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
699         return 0;
700 
701-    srv_debug(1, xs_fmt("mastoapi_post_handler %s", q_path));
702-
703     int status    = 404;
704     xs *args      = NULL;
705-    char *i_ctype = xs_dict_get(req, "content-type");
706+    const char *i_ctype = xs_dict_get(req, "content-type");
707 
708     if (i_ctype && xs_startswith(i_ctype, "application/json")) {
709         if (!xs_is_null(payload))
710@@ -2238,7 +2433,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
711                 }
712 
713                 xs_list *p = mi;
714-                xs_str *v;
715+                const xs_str *v;
716 
717                 while (xs_list_iter(&p, &v)) {
718                     xs *l    = xs_list_new();
719@@ -2296,7 +2491,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
720                 mid = MID_TO_MD5(mid);
721 
722                 if (valid_status(timeline_get_by_md5(&snac, mid, &msg))) {
723-                    char *id = xs_dict_get(msg, "id");
724+                    const char *id = xs_dict_get(msg, "id");
725 
726                     if (op == NULL) {
727                         /* no operation (?) */
728@@ -2402,7 +2597,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
729     if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
730         /* I don't know what I'm doing */
731         if (logged_in) {
732-            char *v;
733+            const char *v;
734 
735             xs *wpush = xs_dict_new();
736 
737@@ -2574,7 +2769,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
738                     const char *id   = xs_dict_get(msg, "id");
739                     const char *atto = get_atto(msg);
740 
741-                    xs_list *opts = xs_dict_get(msg, "oneOf");
742+                    const xs_list *opts = xs_dict_get(msg, "oneOf");
743                     if (opts == NULL)
744                         opts = xs_dict_get(msg, "anyOf");
745 
746@@ -2582,15 +2777,16 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
747                     }
748                     else
749                     if (strcmp(op, "votes") == 0) {
750-                        xs_list *choices = xs_dict_get(args, "choices[]");
751+                        const xs_list *choices = xs_dict_get(args, "choices[]");
752 
753                         if (xs_is_null(choices))
754                             choices = xs_dict_get(args, "choices");
755 
756                         if (xs_type(choices) == XSTYPE_LIST) {
757-                            xs_str *v;
758+                            const xs_str *v;
759 
760-                            while (xs_list_iter(&choices, &v)) {
761+                            int c = 0;
762+                            while (xs_list_next(choices, &v, &c)) {
763                                 int io           = atoi(v);
764                                 const xs_dict *o = xs_list_get(opts, io);
765 
766@@ -2621,19 +2817,73 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
767         else
768             status = 401;
769     }
770+    else
771+    if (strcmp(cmd, "/v1/lists") == 0) {
772+        if (logged_in) {
773+            const char *title = xs_dict_get(args, "title");
774+
775+            if (xs_type(title) == XSTYPE_STRING) {
776+                /* add the list */
777+                xs *out = xs_dict_new();
778+
779+                if (xs_type(list_maint(&snac, title, 1)) == XSTYPE_TRUE) {
780+                    out = xs_dict_append(out, "title", title);
781+                    out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
782+                    out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
783+
784+                    status = 200;
785+                }
786+                else {
787+                    out = xs_dict_append(out, "error", "cannot create list");
788+                    status = 422;
789+                }
790+
791+                *body  = xs_json_dumps(out, 4);
792+                *ctype = "application/json";
793+            }
794+            else
795+                status = 422;
796+        }
797+    }
798+    if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
799+        if (logged_in) {
800+            xs *l = xs_split(cmd, "/");
801+            const char *op = xs_list_get(l, -1);
802+            const char *id = xs_list_get(l, -2);
803+
804+            if (op && id && xs_is_hex(id)) {
805+                if (strcmp(op, "accounts") == 0) {
806+                    const xs_list *accts = xs_dict_get(args, "account_ids[]");
807+                    int c = 0;
808+                    const char *v;
809+
810+                    while (xs_list_next(accts, &v, &c)) {
811+                        list_content(&snac, id, v, 1);
812+                    }
813+
814+                    status = 200;
815+                }
816+            }
817+        }
818+        else
819+            status = 422;
820+    }
821 
822     /* user cleanup */
823     if (logged_in)
824         user_free(&snac);
825 
826+    srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status));
827+
828     return status;
829 }
830 
831 
832 int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
833-                             char **body, int *b_size, char **ctype) {
834-
835-    (void)req;
836+                          const char *payload, int p_size,
837+                          char **body, int *b_size, char **ctype)
838+{
839+    (void)p_size;
840     (void)body;
841     (void)b_size;
842     (void)ctype;
843@@ -2641,13 +2891,76 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
844     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
845         return 0;
846 
847-    srv_debug(1, xs_fmt("mastoapi_delete_handler %s", q_path));
848+    int status    = 404;
849+    xs *args      = NULL;
850+    const char *i_ctype = xs_dict_get(req, "content-type");
851+
852+    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
853+        if (!xs_is_null(payload))
854+            args = xs_json_loads(payload);
855+    }
856+    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
857+    {
858+        // Some apps send form data instead of json so we should cater for those
859+        if (!xs_is_null(payload)) {
860+            xs *upl = xs_url_dec(payload);
861+            args    = xs_url_vars(upl);
862+        }
863+    }
864+    else
865+        args = xs_dup(xs_dict_get(req, "p_vars"));
866+
867+    if (args == NULL)
868+        return 400;
869+
870+    snac snac = {0};
871+    int logged_in = process_auth_token(&snac, req);
872+
873     xs *cmd = xs_replace_n(q_path, "/api", "", 1);
874+
875     if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
876         // pretend we deleted it, since it doesn't exist anyway
877-        return 200;
878+        status = 200;
879+    }
880+    else
881+    if (xs_startswith(cmd, "/v1/lists/")) {
882+        if (logged_in) {
883+            xs *l = xs_split(cmd, "/");
884+            const char *p = xs_list_get(l, -1);
885+
886+            if (p) {
887+                if (strcmp(p, "accounts") == 0) {
888+                    /* delete account from list */
889+                    p = xs_list_get(l, -2);
890+                    const xs_list *accts = xs_dict_get(args, "account_ids[]");
891+                    int c = 0;
892+                    const char *v;
893+
894+                    while (xs_list_next(accts, &v, &c)) {
895+                        list_content(&snac, p, v, 2);
896+                    }
897+                }
898+                else {
899+                    /* delete list */
900+                    if (xs_is_hex(p)) {
901+                        list_maint(&snac, p, 2);
902+                    }
903+                }
904+            }
905+
906+            status = 200;
907+        }
908+        else
909+            status = 401;
910     }
911-    return 0;
912+
913+    /* user cleanup */
914+    if (logged_in)
915+        user_free(&snac);
916+
917+    srv_debug(1, xs_fmt("mastoapi_delete_handler %s %d", q_path, status));
918+
919+    return status;
920 }
921 
922 
923@@ -2663,7 +2976,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
924 
925     int status    = 404;
926     xs *args      = NULL;
927-    char *i_ctype = xs_dict_get(req, "content-type");
928+    const char *i_ctype = xs_dict_get(req, "content-type");
929 
930     if (i_ctype && xs_startswith(i_ctype, "application/json")) {
931         if (!xs_is_null(payload))
932@@ -2770,7 +3083,7 @@ void mastoapi_purge(void)
933     xs *spec   = xs_fmt("%s/app/" "*.json", srv_basedir);
934     xs *files  = xs_glob(spec, 1, 0);
935     xs_list *p = files;
936-    xs_str *v;
937+    const xs_str *v;
938 
939     time_t mt = time(NULL) - 3600;
940 
M snac.h
+41, -28
  1@@ -1,7 +1,7 @@
  2 /* snac - A simple, minimalistic ActivityPub instance */
  3 /* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
  4 
  5-#define VERSION "2.52-dev"
  6+#define VERSION "2.53"
  7 
  8 #define USER_AGENT "snac/" VERSION
  9 
 10@@ -29,6 +29,8 @@ extern int dbglevel;
 11 
 12 #define L(s) (s)
 13 
 14+#define POSTLIKE_OBJECT_TYPE "Note|Question|Page|Article|Video|Event"
 15+
 16 int mkdirx(const char *pathname);
 17 
 18 int valid_status(int status);
 19@@ -67,7 +69,7 @@ void snac_log(snac *user, xs_str *str);
 20 #define snac_debug(user, level, str) do { if (dbglevel >= (level)) \
 21     { snac_log((user), (str)); } } while (0)
 22 
 23-int srv_open(char *basedir, int auto_upgrade);
 24+int srv_open(const char *basedir, int auto_upgrade);
 25 void srv_free(void);
 26 
 27 int user_open(snac *snac, const char *uid);
 28@@ -86,7 +88,7 @@ void srv_archive(const char *direction, const char *url, xs_dict *req,
 29                  const char *body, int b_size);
 30 void srv_archive_error(const char *prefix, const xs_str *err,
 31                        const xs_dict *req, const xs_val *data);
 32-void srv_archive_qitem(char *prefix, xs_dict *q_item);
 33+void srv_archive_qitem(const char *prefix, xs_dict *q_item);
 34 
 35 double mtime_nl(const char *fn, int *n_link);
 36 #define mtime(fn) mtime_nl(fn, NULL)
 37@@ -137,13 +139,13 @@ double timeline_mtime(snac *snac);
 38 int timeline_touch(snac *snac);
 39 int timeline_here(snac *snac, const char *md5);
 40 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
 41-int timeline_del(snac *snac, char *id);
 42+int timeline_del(snac *snac, const char *id);
 43 xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show);
 44 xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show);
 45 int timeline_add(snac *snac, const char *id, const xs_dict *o_msg);
 46 int timeline_admire(snac *snac, const char *id, const char *admirer, int like);
 47 
 48-xs_list *timeline_top_level(snac *snac, xs_list *list);
 49+xs_list *timeline_top_level(snac *snac, const xs_list *list);
 50 xs_list *local_list(snac *snac, int max);
 51 xs_list *timeline_instance_list(int skip, int show);
 52 
 53@@ -172,9 +174,14 @@ void hide(snac *snac, const char *id);
 54 int is_hidden(snac *snac, const char *id);
 55 
 56 void tag_index(const char *id, const xs_dict *obj);
 57-xs_list *tag_search(char *tag, int skip, int show);
 58+xs_list *tag_search(const char *tag, int skip, int show);
 59+
 60+xs_val *list_maint(snac *user, const char *list, int op);
 61+xs_list *list_timeline(snac *user, const char *list, int skip, int show);
 62+xs_val *list_content(snac *user, const char *list_id, const char *actor_md5, int op);
 63+void list_distribute(snac *user, const char *who, const xs_dict *post);
 64 
 65-int actor_add(const char *actor, xs_dict *msg);
 66+int actor_add(const char *actor, const xs_dict *msg);
 67 int actor_get(const char *actor, xs_dict **data);
 68 int actor_get_refresh(snac *user, const char *actor, xs_dict **data);
 69 
 70@@ -209,22 +216,27 @@ int is_instance_blocked(const char *instance);
 71 int instance_block(const char *instance);
 72 int instance_unblock(const char *instance);
 73 
 74-int content_check(const char *file, const xs_dict *msg);
 75+int content_match(const char *file, const xs_dict *msg);
 76+xs_list *content_search(snac *user, const char *regex,
 77+            int priv, int skip, int show, int max_secs, int *timeout);
 78 
 79 void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries);
 80 void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries);
 81 void enqueue_output_raw(const char *keyid, const char *seckey,
 82-                        xs_dict *msg, xs_str *inbox, int retries, int p_status);
 83-void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries, int p_status);
 84-void enqueue_output_by_actor(snac *snac, xs_dict *msg, const xs_str *actor, int retries);
 85-void enqueue_email(xs_str *msg, int retries);
 86+                        const xs_dict *msg, const xs_str *inbox,
 87+                        int retries, int p_status);
 88+void enqueue_output(snac *snac, const xs_dict *msg,
 89+                    const xs_str *inbox, int retries, int p_status);
 90+void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
 91+                             const xs_str *actor, int retries);
 92+void enqueue_email(const xs_str *msg, int retries);
 93 void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id);
 94 void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token);
 95 void enqueue_message(snac *snac, const xs_dict *msg);
 96 void enqueue_close_question(snac *user, const char *id, int end_secs);
 97+void enqueue_object_request(snac *user, const char *id, int forward_secs);
 98 void enqueue_verify_links(snac *user);
 99-void enqueue_actor_refresh(snac *user, const char *actor);
100-void enqueue_request_replies(snac *user, const char *id);
101+void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs);
102 int was_question_voted(snac *user, const char *id);
103 
104 xs_list *user_queue(snac *snac);
105@@ -237,16 +249,16 @@ void purge_all(void);
106 
107 xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
108                             const char *method, const char *url,
109-                            xs_dict *headers,
110+                            const xs_dict *headers,
111                             const char *body, int b_size,
112                             int *status, xs_str **payload, int *p_size,
113                             int timeout);
114 xs_dict *http_signed_request(snac *snac, const char *method, const char *url,
115-                            xs_dict *headers,
116+                            const xs_dict *headers,
117                             const char *body, int b_size,
118                             int *status, xs_str **payload, int *p_size,
119                             int timeout);
120-int check_signature(xs_dict *req, xs_str **err);
121+int check_signature(const xs_dict *req, xs_str **err);
122 
123 srv_state *srv_state_op(xs_str **fname, int op);
124 void httpd(void);
125@@ -260,21 +272,21 @@ const char *default_avatar_base64(void);
126 
127 xs_str *process_tags(snac *snac, const char *content, xs_list **tag);
128 
129-char *get_atto(const xs_dict *msg);
130+const char *get_atto(const xs_dict *msg);
131 xs_list *get_attachments(const xs_dict *msg);
132 
133-xs_dict *msg_admiration(snac *snac, char *object, char *type);
134-xs_dict *msg_repulsion(snac *user, char *id, char *type);
135+xs_dict *msg_admiration(snac *snac, const char *object, const char *type);
136+xs_dict *msg_repulsion(snac *user, const char *id, const char *type);
137 xs_dict *msg_create(snac *snac, const xs_dict *object);
138 xs_dict *msg_follow(snac *snac, const char *actor);
139 
140 xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
141-                  xs_str *in_reply_to, xs_list *attach, int priv);
142+                  const xs_str *in_reply_to, const xs_list *attach, int priv);
143 
144-xs_dict *msg_undo(snac *snac, char *object);
145-xs_dict *msg_delete(snac *snac, char *id);
146+xs_dict *msg_undo(snac *snac, const xs_val *object);
147+xs_dict *msg_delete(snac *snac, const char *id);
148 xs_dict *msg_actor(snac *snac);
149-xs_dict *msg_update(snac *snac, xs_dict *object);
150+xs_dict *msg_update(snac *snac, const xs_dict *object);
151 xs_dict *msg_ping(snac *user, const char *rcpt);
152 xs_dict *msg_pong(snac *user, const char *rcpt, const char *object);
153 xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
154@@ -282,7 +294,6 @@ xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
155 
156 int activitypub_request(snac *snac, const char *url, xs_dict **data);
157 int actor_request(snac *user, const char *actor, xs_dict **data);
158-void timeline_request_replies(snac *user, const char *id);
159 int send_to_inbox_raw(const char *keyid, const char *seckey,
160                   const xs_str *inbox, const xs_dict *msg,
161                   xs_val **payload, int *p_size, int timeout);
162@@ -312,13 +323,14 @@ xs_str *encode_html(const char *str);
163 
164 xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
165                       int skip, int show, int show_more,
166-                      char *tag, char *page, int utl);
167+                      char *title, char *page, int utl);
168 
169 int html_get_handler(const xs_dict *req, const char *q_path,
170                      char **body, int *b_size, char **ctype, xs_str **etag);
171 int html_post_handler(const xs_dict *req, const char *q_path,
172                       char *payload, int p_size,
173                       char **body, int *b_size, char **ctype);
174+xs_str *timeline_to_rss(snac *user, const xs_list *timeline, char *title, char *link, char *desc);
175 
176 int snac_init(const char *_basedir);
177 int adduser(const char *uid);
178@@ -337,11 +349,12 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
179                        char **body, int *b_size, char **ctype);
180 int mastoapi_get_handler(const xs_dict *req, const char *q_path,
181                          char **body, int *b_size, char **ctype);
182-int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
183-                         char **body, int *b_size, char **ctype);
184 int mastoapi_post_handler(const xs_dict *req, const char *q_path,
185                           const char *payload, int p_size,
186                           char **body, int *b_size, char **ctype);
187+int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
188+                          const char *payload, int p_size,
189+                          char **body, int *b_size, char **ctype);
190 int mastoapi_put_handler(const xs_dict *req, const char *q_path,
191                           const char *payload, int p_size,
192                           char **body, int *b_size, char **ctype);
+49, -24
  1@@ -18,7 +18,7 @@ int snac_upgrade(xs_str **error)
  2     double f = 0.0;
  3 
  4     for (;;) {
  5-        char *layout = xs_dict_get(srv_config, "layout");
  6+        const char *layout = xs_dict_get(srv_config, "layout");
  7         double nf;
  8 
  9         f = nf = xs_number_get(layout);
 10@@ -43,7 +43,8 @@ int snac_upgrade(xs_str **error)
 11         else
 12         if (f < 2.2) {
 13             xs *users = user_list();
 14-            char *p, *v;
 15+            char *p;
 16+            const char *v;
 17 
 18             p = users;
 19             while (xs_list_iter(&p, &v)) {
 20@@ -52,12 +53,13 @@ int snac_upgrade(xs_str **error)
 21                 if (user_open(&snac, v)) {
 22                     xs *spec = xs_fmt("%s/actors/" "*.json", snac.basedir);
 23                     xs *list = xs_glob(spec, 0, 0);
 24-                    char *g, *fn;
 25+                    char *g;
 26+                    const char *fn;
 27 
 28                     g = list;
 29                     while (xs_list_iter(&g, &fn)) {
 30-                        xs *l   = xs_split(fn, "/");
 31-                        char *b = xs_list_get(l, -1);
 32+                        xs *l = xs_split(fn, "/");
 33+                        const char *b = xs_list_get(l, -1);
 34                         xs *dir = xs_fmt("%s/object/%c%c", srv_basedir, b[0], b[1]);
 35                         xs *nfn = xs_fmt("%s/%s", dir, b);
 36 
 37@@ -77,14 +79,16 @@ int snac_upgrade(xs_str **error)
 38         else
 39         if (f < 2.3) {
 40             xs *users = user_list();
 41-            char *p, *v;
 42+            char *p;
 43+            const char *v;
 44 
 45             p = users;
 46             while (xs_list_iter(&p, &v)) {
 47                 snac snac;
 48 
 49                 if (user_open(&snac, v)) {
 50-                    char *p, *v;
 51+                    char *p;
 52+                    const char *v;
 53                     xs *dir = xs_fmt("%s/hidden", snac.basedir);
 54 
 55                     /* create the hidden directory */
 56@@ -109,7 +113,8 @@ int snac_upgrade(xs_str **error)
 57         else
 58         if (f < 2.4) {
 59             xs *users = user_list();
 60-            char *p, *v;
 61+            char *p;
 62+            const char *v;
 63 
 64             p = users;
 65             while (xs_list_iter(&p, &v)) {
 66@@ -132,7 +137,8 @@ int snac_upgrade(xs_str **error)
 67         if (f < 2.5) {
 68             /* upgrade followers */
 69             xs *users = user_list();
 70-            char *p, *v;
 71+            char *p;
 72+            const char *v;
 73 
 74             p = users;
 75             while (xs_list_iter(&p, &v)) {
 76@@ -141,7 +147,8 @@ int snac_upgrade(xs_str **error)
 77                 if (user_open(&snac, v)) {
 78                     xs *spec = xs_fmt("%s/followers/" "*.json", snac.basedir);
 79                     xs *dir  = xs_glob(spec, 0, 0);
 80-                    char *p, *v;
 81+                    char *p;
 82+                    const char *v;
 83 
 84                     p = dir;
 85                     while (xs_list_iter(&p, &v)) {
 86@@ -152,12 +159,12 @@ int snac_upgrade(xs_str **error)
 87                             xs *o = xs_json_loads(s);
 88                             fclose(f);
 89 
 90-                            char *type = xs_dict_get(o, "type");
 91+                            const char *type = xs_dict_get(o, "type");
 92 
 93                             if (!xs_is_null(type) && strcmp(type, "Follow") == 0) {
 94                                 unlink(v);
 95 
 96-                                char *actor = xs_dict_get(o, "actor");
 97+                                const char *actor = xs_dict_get(o, "actor");
 98 
 99                                 if (!xs_is_null(actor))
100                                     follower_add(&snac, actor);
101@@ -175,7 +182,8 @@ int snac_upgrade(xs_str **error)
102         if (f < 2.6) {
103             /* upgrade local/ to public/ */
104             xs *users = user_list();
105-            char *p, *v;
106+            char *p;
107+            const char *v;
108 
109             p = users;
110             while (xs_list_iter(&p, &v)) {
111@@ -184,7 +192,8 @@ int snac_upgrade(xs_str **error)
112                 if (user_open(&snac, v)) {
113                     xs *spec = xs_fmt("%s/local/" "*.json", snac.basedir);
114                     xs *dir  = xs_glob(spec, 0, 0);
115-                    char *p, *v;
116+                    char *p;
117+                    const char *v;
118 
119                     p = dir;
120                     while (xs_list_iter(&p, &v)) {
121@@ -198,22 +207,29 @@ int snac_upgrade(xs_str **error)
122                             xs *meta = xs_dup(xs_dict_get(o, "_snac"));
123                             o = xs_dict_del(o, "_snac");
124 
125-                            char *id = xs_dict_get(o, "id");
126+                            const char *id = xs_dict_get(o, "id");
127 
128                             /* store object */
129                             object_add_ow(id, o);
130 
131                             /* if it's from us, add to public */
132                             if (xs_startswith(id, snac.actor)) {
133-                                char *p, *v;
134+                                const xs_list *p;
135+                                const char *v;
136+                                int c;
137 
138                                 object_user_cache_add(&snac, id, "public");
139 
140                                 p = xs_dict_get(meta, "announced_by");
141-                                while (xs_list_iter(&p, &v))
142+
143+                                c = 0;
144+                                while (xs_list_next(p, &v, &c))
145                                     object_admire(id, v, 0);
146+
147                                 p = xs_dict_get(meta, "liked_by");
148-                                while (xs_list_iter(&p, &v))
149+
150+                                c = 0;
151+                                while (xs_list_next(p, &v, &c))
152                                     object_admire(id, v, 1);
153                             }
154 
155@@ -234,7 +250,8 @@ int snac_upgrade(xs_str **error)
156         if (f < 2.7) {
157             /* upgrade timeline/ to private/ */
158             xs *users = user_list();
159-            char *p, *v;
160+            char *p;
161+            const char *v;
162 
163             p = users;
164             while (xs_list_iter(&p, &v)) {
165@@ -243,7 +260,8 @@ int snac_upgrade(xs_str **error)
166                 if (user_open(&snac, v)) {
167                     xs *spec = xs_fmt("%s/timeline/" "*.json", snac.basedir);
168                     xs *dir  = xs_glob(spec, 0, 0);
169-                    char *p, *v;
170+                    char *p;
171+                    const char *v;
172 
173                     p = dir;
174                     while (xs_list_iter(&p, &v)) {
175@@ -257,21 +275,28 @@ int snac_upgrade(xs_str **error)
176                             xs *meta = xs_dup(xs_dict_get(o, "_snac"));
177                             o = xs_dict_del(o, "_snac");
178 
179-                            char *id = xs_dict_get(o, "id");
180+                            const char *id = xs_dict_get(o, "id");
181 
182                             /* store object */
183                             object_add_ow(id, o);
184 
185                             {
186-                                char *p, *v;
187+                                const xs_list *p;
188+                                const char *v;
189+                                int c = 0;
190 
191                                 object_user_cache_add(&snac, id, "private");
192 
193                                 p = xs_dict_get(meta, "announced_by");
194-                                while (xs_list_iter(&p, &v))
195+
196+                                c = 0;
197+                                while (xs_list_next(p, &v, &c))
198                                     object_admire(id, v, 0);
199+
200                                 p = xs_dict_get(meta, "liked_by");
201-                                while (xs_list_iter(&p, &v))
202+
203+                                c = 0;
204+                                while (xs_list_next(p, &v, &c))
205                                     object_admire(id, v, 1);
206                             }
207 
+10, -7
 1@@ -25,6 +25,8 @@ static const char *default_srv_config = "{"
 2     "\"dbglevel\":             0,"
 3     "\"queue_retry_minutes\":  2,"
 4     "\"queue_retry_max\":      10,"
 5+    "\"queue_timeout\":        6,"
 6+    "\"queue_timeout_2\":      8,"
 7     "\"cssurls\":              [\"\"],"
 8     "\"max_timeline_entries\": 50,"
 9     "\"timeline_purge_days\":  120,"
10@@ -34,6 +36,7 @@ static const char *default_srv_config = "{"
11     "\"admin_account\":        \"\","
12     "\"title\":                \"\","
13     "\"short_description\":    \"\","
14+    "\"protocol\":             \"https\","
15     "\"fastcgi\":              false"
16     "}";
17 
18@@ -46,7 +49,7 @@ static const char *default_css =
19     ".snac-top-user { text-align: center; padding-bottom: 2em }\n"
20     ".snac-top-user-name { font-size: 200% }\n"
21     ".snac-top-user-id { font-size: 150% }\n"
22-    ".snac-avatar { float: left; height: 2.5em; padding: 0.25em }\n"
23+    ".snac-avatar { float: left; height: 2.5em; width: 2.5em; padding: 0.25em }\n"
24     ".snac-author { font-size: 90%; text-decoration: none }\n"
25     ".snac-author-tag { font-size: 80% }\n"
26     ".snac-pubdate { color: #a0a0a0; font-size: 90% }\n"
27@@ -355,7 +358,7 @@ void rm_rf(const char *dir)
28     xs *d = xs_str_cat(xs_dup(dir), "/" "*");
29     xs *l = xs_glob(d, 0, 0);
30     xs_list *p = l;
31-    xs_str *v;
32+    const xs_str *v;
33 
34     if (dbglevel >= 1)
35         printf("Deleting directory %s\n", dir);
36@@ -390,7 +393,7 @@ int deluser(snac *user)
37     int ret = 0;
38     xs *fwers = following_list(user);
39     xs_list *p = fwers;
40-    xs_str *v;
41+    const xs_str *v;
42 
43     while (xs_list_iter(&p, &v)) {
44         xs *object = NULL;
45@@ -415,8 +418,8 @@ int deluser(snac *user)
46 void verify_links(snac *user)
47 /* verifies a user's links */
48 {
49-    xs_dict *p = xs_dict_get(user->config, "metadata");
50-    char *k, *v;
51+    const xs_dict *p = xs_dict_get(user->config, "metadata");
52+    const char *k, *v;
53     int changed = 0;
54 
55     xs *headers = xs_dict_new();
56@@ -446,7 +449,7 @@ void verify_links(snac *user)
57         xs *ls = xs_regex_select(payload, "< *(a|link) +[^>]+>");
58 
59         xs_list *lp = ls;
60-        char *ll;
61+        const char *ll;
62         int vfied = 0;
63 
64         while (!vfied && xs_list_iter(&lp, &ll)) {
65@@ -460,7 +463,7 @@ void verify_links(snac *user)
66             xs *href = NULL;
67             int is_rel_me = 0;
68             xs_list *pr = r;
69-            char *ar;
70+            const char *ar;
71 
72             while (xs_list_iter(&pr, &ar)) {
73                 xs *nq = xs_dup(ar);
+31, -18
  1@@ -16,12 +16,13 @@ int webfinger_request_signed(snac *snac, const char *qs, char **actor, char **us
  2     int p_size = 0;
  3     xs *headers = xs_dict_new();
  4     xs *l = NULL;
  5-    xs_str *host = NULL;
  6+    const char *host = NULL;
  7     xs *resource = NULL;
  8 
  9-    if (xs_startswith(qs, "https:/" "/")) {
 10+    if (xs_startswith(qs, "https:/") || xs_startswith(qs, "http:/")) {
 11         /* actor query: pick the host */
 12-        xs *s = xs_replace_n(qs, "https:/" "/", "", 1);
 13+        xs *s1 = xs_replace_n(qs, "http:/" "/", "", 1);
 14+        xs *s = xs_replace_n(s1, "https:/" "/", "", 1);
 15 
 16         l = xs_split_n(s, "/", 1);
 17 
 18@@ -69,7 +70,9 @@ int webfinger_request_signed(snac *snac, const char *qs, char **actor, char **us
 19                                        &payload, &p_size, &ctype);
 20     }
 21     else {
 22-        xs *url = xs_fmt("https:/" "/%s/.well-known/webfinger?resource=%s", host, resource);
 23+        const char *proto = xs_dict_get_def(srv_config, "protocol", "https");
 24+
 25+        xs *url = xs_fmt("%s:/" "/%s/.well-known/webfinger?resource=%s", proto, host, resource);
 26 
 27         if (snac == NULL)
 28             xs_http_request("GET", url, headers, NULL, 0, &status, &payload, &p_size, 0);
 29@@ -84,22 +87,24 @@ int webfinger_request_signed(snac *snac, const char *qs, char **actor, char **us
 30 
 31     if (obj) {
 32         if (user != NULL) {
 33-            char *subject = xs_dict_get(obj, "subject");
 34+            const char *subject = xs_dict_get(obj, "subject");
 35 
 36             if (subject)
 37                 *user = xs_replace_n(subject, "acct:", "", 1);
 38         }
 39 
 40         if (actor != NULL) {
 41-            char *list = xs_dict_get(obj, "links");
 42-            char *v;
 43+            const xs_list *list = xs_dict_get(obj, "links");
 44+            int c = 0;
 45+            const char *v;
 46 
 47-            while (xs_list_iter(&list, &v)) {
 48+            while (xs_list_next(list, &v, &c)) {
 49                 if (xs_type(v) == XSTYPE_DICT) {
 50-                    char *type = xs_dict_get(v, "type");
 51+                    const char *type = xs_dict_get(v, "type");
 52 
 53                     if (type && (strcmp(type, "application/activity+json") == 0 ||
 54-                                strcmp(type, "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") == 0)) {
 55+                                 strcmp(type, "application/ld+json; profile=\"https:/"
 56+                                    "/www.w3.org/ns/activitystreams\"") == 0)) {
 57                         *actor = xs_dup(xs_dict_get(v, "href"));
 58                         break;
 59                     }
 60@@ -130,8 +135,8 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
 61     if (strcmp(q_path, "/.well-known/webfinger") != 0)
 62         return 0;
 63 
 64-    char *q_vars   = xs_dict_get(req, "q_vars");
 65-    char *resource = xs_dict_get(q_vars, "resource");
 66+    const char *q_vars   = xs_dict_get(req, "q_vars");
 67+    const char *resource = xs_dict_get(q_vars, "resource");
 68 
 69     if (resource == NULL)
 70         return 400;
 71@@ -139,10 +144,10 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
 72     snac snac;
 73     int found = 0;
 74 
 75-    if (xs_startswith(resource, "https:/" "/")) {
 76+    if (xs_startswith(resource, "https:/") || xs_startswith(resource, "http:/")) {
 77         /* actor search: find a user with this actor */
 78         xs *l = xs_split(resource, "/");
 79-        char *uid = xs_list_get(l, -1);
 80+        const char *uid = xs_list_get(l, -1);
 81 
 82         if (uid)
 83             found = user_open(&snac, uid);
 84@@ -160,8 +165,8 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
 85         l = xs_split_n(an, "@", 1);
 86 
 87         if (xs_list_len(l) == 2) {
 88-            char *uid  = xs_list_get(l, 0);
 89-            char *host = xs_list_get(l, 1);
 90+            const char *uid  = xs_list_get(l, 0);
 91+            const char *host = xs_list_get(l, 1);
 92 
 93             if (strcmp(host, xs_dict_get(srv_config, "host")) == 0)
 94                 found = user_open(&snac, uid);
 95@@ -185,13 +190,19 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
 96 
 97         links = xs_list_append(links, aaj);
 98 
 99+        /* duplicate with the ld+json type */
100+        aaj = xs_dict_set(aaj, "type", "application/ld+json; profile=\"https:/"
101+                                    "/www.w3.org/ns/activitystreams\"");
102+
103+        links = xs_list_append(links, aaj);
104+
105         prof = xs_dict_append(prof, "rel", "http://webfinger.net/rel/profile-page");
106         prof = xs_dict_append(prof, "type", "text/html");
107         prof = xs_dict_append(prof, "href", snac.actor);
108 
109         links = xs_list_append(links, prof);
110 
111-        char *avatar = xs_dict_get(snac.config, "avatar");
112+        const char *avatar = xs_dict_get(snac.config, "avatar");
113         if (!xs_is_null(avatar) && *avatar) {
114             xs *d = xs_dict_new();
115 
116@@ -211,10 +222,12 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
117 
118         status = 200;
119         *body  = j;
120-        *ctype = "application/json";
121+        *ctype = "application/jrd+json";
122     }
123     else
124         status = 404;
125 
126+    srv_debug(1, xs_fmt("webfinger_get_handler resource=%s %d", resource, status));
127+
128     return status;
129 }
M xs.h
+151, -103
  1@@ -21,8 +21,8 @@ typedef enum {
  2     XSTYPE_FALSE  = 0x15,       /* Boolean */
  3     XSTYPE_LIST   = 0x1d,       /* Sequence of LITEMs up to EOM (with size) */
  4     XSTYPE_LITEM  = 0x1f,       /* Element of a list (any type) */
  5-    XSTYPE_DICT   = 0x1c,       /* Sequence of DITEMs up to EOM (with size) */
  6-    XSTYPE_DITEM  = 0x1e,       /* Element of a dict (STRING key + any type) */
  7+    XSTYPE_DICT   = 0x1c,       /* Sequence of KEYVALs up to EOM (with size) */
  8+    XSTYPE_KEYVAL = 0x1e,       /* key + value (STRING key + any type) */
  9     XSTYPE_EOM    = 0x19,       /* End of Multiple (LIST or DICT) */
 10     XSTYPE_DATA   = 0x10        /* A block of anonymous data */
 11 } xstype;
 12@@ -32,6 +32,7 @@ typedef enum {
 13 typedef char xs_val;
 14 typedef char xs_str;
 15 typedef char xs_list;
 16+typedef char xs_keyval;
 17 typedef char xs_dict;
 18 typedef char xs_number;
 19 typedef char xs_data;
 20@@ -45,6 +46,10 @@ typedef char xs_data;
 21 /* not really all, just very much */
 22 #define XS_ALL 0xfffffff
 23 
 24+#ifndef xs_countof
 25+#define xs_countof(a) (sizeof((a)) / sizeof((*a)))
 26+#endif
 27+
 28 void *xs_free(void *ptr);
 29 void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func);
 30 #define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __FUNCTION__)
 31@@ -89,9 +94,10 @@ xs_list *xs_list_new(void);
 32 xs_list *xs_list_append_m(xs_list *list, const char *mem, int dsz);
 33 xs_list *_xs_list_append(xs_list *list, const xs_val *vals[]);
 34 #define xs_list_append(list, ...) _xs_list_append(list, (const xs_val *[]){ __VA_ARGS__, NULL })
 35-int xs_list_iter(xs_list **list, xs_val **value);
 36+int xs_list_iter(xs_list **list, const xs_val **value);
 37+int xs_list_next(const xs_list *list, const xs_val **value, int *ctxt);
 38 int xs_list_len(const xs_list *list);
 39-xs_val *xs_list_get(const xs_list *list, int num);
 40+const xs_val *xs_list_get(const xs_list *list, int num);
 41 xs_list *xs_list_del(xs_list *list, int num);
 42 xs_list *xs_list_insert(xs_list *list, int num, const xs_val *data);
 43 xs_list *xs_list_set(xs_list *list, int num, const xs_val *data);
 44@@ -104,17 +110,20 @@ xs_list *xs_split_n(const char *str, const char *sep, int times);
 45 #define xs_split(str, sep) xs_split_n(str, sep, XS_ALL)
 46 xs_list *xs_list_cat(xs_list *l1, const xs_list *l2);
 47 
 48+int xs_keyval_size(const xs_str *key, const xs_val *value);
 49+xs_str *xs_keyval_key(const xs_keyval *keyval);
 50+xs_val *xs_keyval_value(const xs_keyval *keyval);
 51+xs_keyval *xs_keyval_make(xs_keyval *keyval, const xs_str *key, const xs_val *value);
 52+
 53 xs_dict *xs_dict_new(void);
 54-xs_dict *xs_dict_append_m(xs_dict *dict, const xs_str *key, const xs_val *mem, int dsz);
 55-#define xs_dict_append(dict, key, data) xs_dict_append_m(dict, key, data, xs_size(data))
 56-xs_dict *xs_dict_prepend_m(xs_dict *dict, const xs_str *key, const xs_val *mem, int dsz);
 57-#define xs_dict_prepend(dict, key, data) xs_dict_prepend_m(dict, key, data, xs_size(data))
 58-int xs_dict_iter(xs_dict **dict, xs_str **key, xs_val **value);
 59-int xs_dict_next(const xs_dict *dict, xs_str **key, xs_val **value, int *ctxt);
 60-xs_val *xs_dict_get_def(const xs_dict *dict, const xs_str *key, const xs_val *def);
 61+xs_dict *xs_dict_append(xs_dict *dict, const xs_str *key, const xs_val *value);
 62+xs_dict *xs_dict_prepend(xs_dict *dict, const xs_str *key, const xs_val *value);
 63+int xs_dict_next(const xs_dict *dict, const xs_str **key, const xs_val **value, int *ctxt);
 64+const xs_val *xs_dict_get_def(const xs_dict *dict, const xs_str *key, const xs_val *def);
 65 #define xs_dict_get(dict, key) xs_dict_get_def(dict, key, NULL)
 66 xs_dict *xs_dict_del(xs_dict *dict, const xs_str *key);
 67 xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *data);
 68+xs_dict *xs_dict_gc(xs_dict *dict);
 69 
 70 xs_val *xs_val_new(xstype t);
 71 xs_number *xs_number_new(double f);
 72@@ -242,7 +251,7 @@ xstype xs_type(const xs_val *data)
 73     case XSTYPE_LIST:
 74     case XSTYPE_LITEM:
 75     case XSTYPE_DICT:
 76-    case XSTYPE_DITEM:
 77+    case XSTYPE_KEYVAL:
 78     case XSTYPE_NUMBER:
 79     case XSTYPE_EOM:
 80     case XSTYPE_DATA:
 81@@ -260,7 +269,7 @@ xstype xs_type(const xs_val *data)
 82 void _xs_put_size(xs_val *ptr, int i)
 83 /* must match _XS_TYPE_SIZE */
 84 {
 85-    memcpy(ptr, &i, sizeof(i));
 86+    memcpy(ptr + 1, &i, sizeof(i));
 87 }
 88 
 89 
 90@@ -294,7 +303,7 @@ int xs_size(const xs_val *data)
 91 
 92         break;
 93 
 94-    case XSTYPE_DITEM:
 95+    case XSTYPE_KEYVAL:
 96         /* calculate the size of the key and the value */
 97         p = data + 1;
 98         p += xs_size(p);
 99@@ -378,7 +387,7 @@ xs_val *xs_expand(xs_val *data, int offset, int size)
100     if (xs_type(data) == XSTYPE_LIST ||
101         xs_type(data) == XSTYPE_DICT ||
102         xs_type(data) == XSTYPE_DATA)
103-        _xs_put_size(data + 1, sz);
104+        _xs_put_size(data, sz);
105 
106     return data;
107 }
108@@ -403,7 +412,7 @@ xs_val *xs_collapse(xs_val *data, int offset, int size)
109     if (xs_type(data) == XSTYPE_LIST ||
110         xs_type(data) == XSTYPE_DICT ||
111         xs_type(data) == XSTYPE_DATA)
112-        _xs_put_size(data + 1, sz);
113+        _xs_put_size(data, sz);
114 
115     return xs_realloc(data, _xs_blk_size(sz));
116 }
117@@ -664,10 +673,10 @@ xs_list *xs_list_new(void)
118 {
119     int sz = 1 + _XS_TYPE_SIZE + 1;
120     xs_list *l = xs_realloc(NULL, sz);
121-    memset(l, '\0', sz);
122+    memset(l, XSTYPE_EOM, sz);
123 
124     l[0] = XSTYPE_LIST;
125-    _xs_put_size(&l[1], sz);
126+    _xs_put_size(l, sz);
127 
128     return l;
129 }
130@@ -717,7 +726,7 @@ xs_list *_xs_list_append(xs_list *list, const xs_val *vals[])
131 }
132 
133 
134-int xs_list_iter(xs_list **list, xs_val **value)
135+int xs_list_iter(xs_list **list, const xs_val **value)
136 /* iterates a list value */
137 {
138     int goon = 1;
139@@ -748,23 +757,58 @@ int xs_list_iter(xs_list **list, xs_val **value)
140 }
141 
142 
143+int xs_list_next(const xs_list *list, const xs_val **value, int *ctxt)
144+/* iterates a list, with context */
145+{
146+    if (xs_type(list) != XSTYPE_LIST)
147+        return 0;
148+
149+    int goon = 1;
150+
151+    const char *p = list;
152+
153+    /* skip the start of the list */
154+    if (*ctxt == 0)
155+        *ctxt = 1 + _XS_TYPE_SIZE;
156+
157+    p += *ctxt;
158+
159+    /* an element? */
160+    if (xs_type(p) == XSTYPE_LITEM) {
161+        p++;
162+
163+        *value = p;
164+
165+        p += xs_size(*value);
166+    }
167+    else {
168+        /* end of list */
169+        goon = 0;
170+    }
171+
172+    /* update the context */
173+    *ctxt = p - list;
174+
175+    return goon;
176+}
177+
178+
179 int xs_list_len(const xs_list *list)
180 /* returns the number of elements in the list */
181 {
182     XS_ASSERT_TYPE_NULL(list, XSTYPE_LIST);
183 
184-    int c = 0;
185-    xs_list *p = (xs_list *)list;
186-    xs_val *v;
187+    int c = 0, ct = 0;
188+    const xs_val *v;
189 
190-    while (xs_list_iter(&p, &v))
191+    while (xs_list_next(list, &v, &ct))
192         c++;
193 
194     return c;
195 }
196 
197 
198-xs_val *xs_list_get(const xs_list *list, int num)
199+const xs_val *xs_list_get(const xs_list *list, int num)
200 /* returns the element #num */
201 {
202     XS_ASSERT_TYPE(list, XSTYPE_LIST);
203@@ -772,11 +816,10 @@ xs_val *xs_list_get(const xs_list *list, int num)
204     if (num < 0)
205         num = xs_list_len(list) + num;
206 
207-    int c = 0;
208-    xs_list *p = (xs_list *)list;
209-    xs_val *v;
210+    int c = 0, ct = 0;
211+    const xs_val *v;
212 
213-    while (xs_list_iter(&p, &v)) {
214+    while (xs_list_next(list, &v, &ct)) {
215         if (c == num)
216             return v;
217 
218@@ -792,7 +835,7 @@ xs_list *xs_list_del(xs_list *list, int num)
219 {
220     XS_ASSERT_TYPE(list, XSTYPE_LIST);
221 
222-    xs_val *v;
223+    const xs_val *v;
224 
225     if ((v = xs_list_get(list, num)) != NULL)
226         list = xs_collapse(list, v - 1 - list, xs_size(v - 1));
227@@ -806,7 +849,7 @@ xs_list *xs_list_insert(xs_list *list, int num, const xs_val *data)
228 {
229     XS_ASSERT_TYPE(list, XSTYPE_LIST);
230 
231-    xs_val *v;
232+    const xs_val *v;
233     int offset;
234 
235     if ((v = xs_list_get(list, num)) != NULL)
236@@ -835,16 +878,16 @@ xs_list *xs_list_dequeue(xs_list *list, xs_val **data, int last)
237 {
238     XS_ASSERT_TYPE(list, XSTYPE_LIST);
239 
240-    xs_list *p = list;
241-    xs_val *v  = NULL;
242+    int ct = 0;
243+    const xs_val *v = NULL;
244 
245     if (!last) {
246         /* get the first */
247-        xs_list_iter(&p, &v);
248+        xs_list_next(list, &v, &ct);
249     }
250     else {
251         /* iterate to the end */
252-        while (xs_list_iter(&p, &v));
253+        while (xs_list_next(list, &v, &ct));
254     }
255 
256     if (v != NULL) {
257@@ -864,11 +907,11 @@ int xs_list_in(const xs_list *list, const xs_val *val)
258     XS_ASSERT_TYPE_NULL(list, XSTYPE_LIST);
259 
260     int n = 0;
261-    xs_list *p = (xs_list *)list;
262-    xs_val *v;
263+    int ct = 0;
264+    const xs_val *v;
265     int sz = xs_size(val);
266 
267-    while (xs_list_iter(&p, &v)) {
268+    while (xs_list_next(list, &v, &ct)) {
269         if (sz == xs_size(v) && memcmp(val, v, sz) == 0)
270             return n;
271 
272@@ -885,13 +928,13 @@ xs_str *xs_join(const xs_list *list, const char *sep)
273     XS_ASSERT_TYPE(list, XSTYPE_LIST);
274 
275     xs_str *s = NULL;
276-    xs_list *p = (xs_list *)list;
277-    xs_val *v;
278+    const xs_val *v;
279     int c = 0;
280+    int ct = 0;
281     int offset = 0;
282     int ssz = strlen(sep);
283 
284-    while (xs_list_iter(&p, &v)) {
285+    while (xs_list_next(list, &v, &ct)) {
286         /* refuse to join non-string values */
287         if (xs_type(v) == XSTYPE_STRING) {
288             int sz;
289@@ -961,6 +1004,40 @@ xs_list *xs_list_cat(xs_list *l1, const xs_list *l2)
290 }
291 
292 
293+/** keyvals **/
294+
295+int xs_keyval_size(const xs_str *key, const xs_val *value)
296+/* returns the needed size for a keyval */
297+{
298+    return 1 + xs_size(key) + xs_size(value);
299+}
300+
301+
302+xs_str *xs_keyval_key(const xs_keyval *keyval)
303+/* returns a pointer to the key of the keyval */
304+{
305+    return (xs_str *)&keyval[1];
306+}
307+
308+
309+xs_val *xs_keyval_value(const xs_keyval *keyval)
310+/* returns a pointer to the value of the keyval */
311+{
312+    return (xs_val *)&keyval[1 + xs_size(xs_keyval_key(keyval))];
313+}
314+
315+
316+xs_keyval *xs_keyval_make(xs_keyval *keyval, const xs_str *key, const xs_val *value)
317+/* builds a keyval into mem (should have enough size) */
318+{
319+    keyval[0] = XSTYPE_KEYVAL;
320+    memcpy(xs_keyval_key(keyval),   key,   xs_size(key));
321+    memcpy(xs_keyval_value(keyval), value, xs_size(value));
322+
323+    return keyval;
324+}
325+
326+
327 /** dicts **/
328 
329 xs_dict *xs_dict_new(void)
330@@ -968,87 +1045,47 @@ xs_dict *xs_dict_new(void)
331 {
332     int sz = 1 + _XS_TYPE_SIZE + 1;
333     xs_dict *d = xs_realloc(NULL, sz);
334-    memset(d, '\0', sz);
335+    memset(d, XSTYPE_EOM, sz);
336 
337     d[0] = XSTYPE_DICT;
338-    _xs_put_size(&d[1], sz);
339+    _xs_put_size(d, sz);
340 
341     return d;
342 }
343 
344 
345-xs_dict *_xs_dict_write_ditem(xs_dict *dict, int offset, const xs_str *key,
346-                          const xs_val *data, int dsz)
347-/* inserts a memory block into the dict */
348+xs_dict *_xs_dict_write_keyval(xs_dict *dict, int offset, const xs_str *key, const xs_val *value)
349+/* adds a new keyval to the dict */
350 {
351     XS_ASSERT_TYPE(dict, XSTYPE_DICT);
352     XS_ASSERT_TYPE(key, XSTYPE_STRING);
353 
354-    if (data == NULL) {
355-        data = xs_stock(XSTYPE_NULL);
356-        dsz  = xs_size(data);
357-    }
358-
359-    int ksz = xs_size(key);
360+    if (value == NULL)
361+        value = xs_stock(XSTYPE_NULL);
362 
363-    dict = xs_expand(dict, offset, 1 + ksz + dsz);
364+    dict = xs_expand(dict, offset, xs_keyval_size(key, value));
365 
366-    dict[offset] = XSTYPE_DITEM;
367-    memcpy(&dict[offset + 1], key, ksz);
368-    memcpy(&dict[offset + 1 + ksz], data, dsz);
369+    xs_keyval_make(&dict[offset], key, value);
370 
371     return dict;
372 }
373 
374 
375-xs_dict *xs_dict_append_m(xs_dict *dict, const xs_str *key, const xs_val *mem, int dsz)
376+xs_dict *xs_dict_append(xs_dict *dict, const xs_str *key, const xs_val *value)
377 /* appends a memory block to the dict */
378 {
379-    return _xs_dict_write_ditem(dict, xs_size(dict) - 1, key, mem, dsz);
380+    return _xs_dict_write_keyval(dict, xs_size(dict) - 1, key, value);
381 }
382 
383 
384-xs_dict *xs_dict_prepend_m(xs_dict *dict, const xs_str *key, const xs_val *mem, int dsz)
385+xs_dict *xs_dict_prepend(xs_dict *dict, const xs_str *key, const xs_val *value)
386 /* prepends a memory block to the dict */
387 {
388-    return _xs_dict_write_ditem(dict, 4, key, mem, dsz);
389-}
390-
391-
392-int xs_dict_iter(xs_dict **dict, xs_str **key, xs_val **value)
393-/* iterates a dict value */
394-{
395-    int goon = 1;
396-
397-    xs_val *p = *dict;
398-
399-    /* skip the start of the list */
400-    if (xs_type(p) == XSTYPE_DICT)
401-        p += 1 + _XS_TYPE_SIZE;
402-
403-    /* an element? */
404-    if (xs_type(p) == XSTYPE_DITEM) {
405-        p++;
406-
407-        *key = p;
408-        p += xs_size(*key);
409-
410-        *value = p;
411-        p += xs_size(*value);
412-    }
413-    else {
414-        /* end of list */
415-        goon = 0;
416-    }
417-
418-    /* store back the pointer */
419-    *dict = p;
420-
421-    return goon;
422+    return _xs_dict_write_keyval(dict, 1 + _XS_TYPE_SIZE, key, value);
423 }
424 
425 
426-int xs_dict_next(const xs_dict *dict, xs_str **key, xs_val **value, int *ctxt)
427+int xs_dict_next(const xs_dict *dict, const xs_str **key, const xs_val **value, int *ctxt)
428 /* iterates a dict, with context */
429 {
430     if (xs_type(dict) != XSTYPE_DICT)
431@@ -1065,7 +1102,7 @@ int xs_dict_next(const xs_dict *dict, xs_str **key, xs_val **value, int *ctxt)
432     p += *ctxt;
433 
434     /* an element? */
435-    if (xs_type(p) == XSTYPE_DITEM) {
436+    if (xs_type(p) == XSTYPE_KEYVAL) {
437         p++;
438 
439         *key = p;
440@@ -1086,14 +1123,14 @@ int xs_dict_next(const xs_dict *dict, xs_str **key, xs_val **value, int *ctxt)
441 }
442 
443 
444-xs_val *xs_dict_get_def(const xs_dict *dict, const xs_str *key, const xs_val *def)
445+const xs_val *xs_dict_get_def(const xs_dict *dict, const xs_str *key, const xs_val *def)
446 /* returns the value directed by key, or the default value */
447 {
448     XS_ASSERT_TYPE(dict, XSTYPE_DICT);
449     XS_ASSERT_TYPE(key, XSTYPE_STRING);
450 
451-    xs_str *k;
452-    xs_val *v;
453+    const xs_str *k;
454+    const xs_val *v;
455     int c = 0;
456 
457     while (xs_dict_next(dict, &k, &v, &c)) {
458@@ -1111,14 +1148,14 @@ xs_dict *xs_dict_del(xs_dict *dict, const xs_str *key)
459     XS_ASSERT_TYPE(dict, XSTYPE_DICT);
460     XS_ASSERT_TYPE(key, XSTYPE_STRING);
461 
462-    xs_str *k;
463-    xs_val *v;
464+    const xs_str *k;
465+    const xs_val *v;
466     int c = 0;
467 
468     while (xs_dict_next(dict, &k, &v, &c)) {
469         if (strcmp(k, key) == 0) {
470             /* the address of the item is just behind the key */
471-            char *i = k - 1;
472+            char *i = (char *)k - 1;
473 
474             dict = xs_collapse(dict, i - dict, xs_size(i));
475             break;
476@@ -1145,6 +1182,14 @@ xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *data)
477 }
478 
479 
480+xs_dict *xs_dict_gc(xs_dict *dict)
481+/* collects garbage (leaked values) inside a dict */
482+{
483+    /* this kind of dicts does not get garbage */
484+    return dict;
485+}
486+
487+
488 /** other values **/
489 
490 xs_val *xs_val_new(xstype t)
491@@ -1195,8 +1240,11 @@ double xs_number_get(const xs_number *v)
492 {
493     double f = 0.0;
494 
495-    if (v != NULL && v[0] == XSTYPE_NUMBER)
496+    if (xs_type(v) == XSTYPE_NUMBER)
497         f = atof(&v[1]);
498+    else
499+    if (xs_type(v) == XSTYPE_STRING)
500+        f = atof(v);
501 
502     return f;
503 }
504@@ -1207,7 +1255,7 @@ const char *xs_number_str(const xs_number *v)
505 {
506     const char *p = NULL;
507 
508-    if (v != NULL && v[0] == XSTYPE_NUMBER)
509+    if (xs_type(v) == XSTYPE_NUMBER)
510         p = &v[1];
511 
512     return p;
513@@ -1227,7 +1275,7 @@ xs_data *xs_data_new(const void *data, int size)
514     v = xs_realloc(NULL, _xs_blk_size(total_size));
515     v[0] = XSTYPE_DATA;
516 
517-    _xs_put_size(v + 1, total_size);
518+    _xs_put_size(v, total_size);
519 
520     memcpy(&v[1 + _XS_TYPE_SIZE], data, size);
521 
+3, -3
 1@@ -28,7 +28,7 @@ static size_t _header_callback(char *buffer, size_t size,
 2     if (xs_str_in(l, ": ") != -1) {
 3         xs *knv = xs_split_n(l, ": ", 1);
 4 
 5-        xs_tolower_i(xs_list_get(knv, 0));
 6+        xs_tolower_i((xs_str *)xs_list_get(knv, 0));
 7 
 8         headers = xs_dict_set(headers, xs_list_get(knv, 0), xs_list_get(knv, 1));
 9     }
10@@ -93,8 +93,8 @@ xs_dict *xs_http_request(const char *method, const char *url,
11     xs_dict *response;
12     CURL *curl;
13     struct curl_slist *list = NULL;
14-    xs_str *k;
15-    xs_val *v;
16+    const xs_str *k;
17+    const xs_val *v;
18     long lstatus = 0;
19     struct _payload_data pd;
20 
+2, -2
 1@@ -293,8 +293,8 @@ void xs_fcgi_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b
 2     struct fcgi_record_header hdr = {0};
 3     struct fcgi_end_request ereq = {0};
 4     xs *out = xs_str_new(NULL);
 5-    xs_str *k;
 6-    xs_str *v;
 7+    const xs_str *k;
 8+    const xs_str *v;
 9 
10     /* no previous id? it's an error */
11     if (fcgi_id == -1)
+17, -17
  1@@ -6,26 +6,26 @@
  2 
  3 typedef struct xs_html xs_html;
  4 
  5-xs_str *xs_html_encode(char *str);
  6+xs_str *xs_html_encode(const char *str);
  7 
  8-xs_html *xs_html_attr(char *key, char *value);
  9-xs_html *xs_html_text(char *content);
 10-xs_html *xs_html_raw(char *content);
 11+xs_html *xs_html_attr(const char *key, const char *value);
 12+xs_html *xs_html_text(const char *content);
 13+xs_html *xs_html_raw(const char *content);
 14 
 15 xs_html *_xs_html_add(xs_html *tag, xs_html *var[]);
 16 #define xs_html_add(tag, ...) _xs_html_add(tag, (xs_html *[]) { __VA_ARGS__, NULL })
 17 
 18-xs_html *_xs_html_tag(char *tag, xs_html *var[]);
 19+xs_html *_xs_html_tag(const char *tag, xs_html *var[]);
 20 #define xs_html_tag(tag, ...) _xs_html_tag(tag, (xs_html *[]) { __VA_ARGS__, NULL })
 21 
 22-xs_html *_xs_html_sctag(char *tag, xs_html *var[]);
 23+xs_html *_xs_html_sctag(const char *tag, xs_html *var[]);
 24 #define xs_html_sctag(tag, ...) _xs_html_sctag(tag, (xs_html *[]) { __VA_ARGS__, NULL })
 25 
 26 xs_html *_xs_html_container(xs_html *var[]);
 27 #define xs_html_container(...) _xs_html_container((xs_html *[]) { __VA_ARGS__, NULL })
 28 
 29 void xs_html_render_f(xs_html *h, FILE *f);
 30-xs_str *xs_html_render_s(xs_html *tag, char *prefix);
 31+xs_str *xs_html_render_s(xs_html *tag, const char *prefix);
 32 #define xs_html_render(tag) xs_html_render_s(tag, NULL)
 33 
 34 
 35@@ -47,16 +47,16 @@ struct xs_html {
 36     xs_html *next;
 37 };
 38 
 39-xs_str *xs_html_encode(char *str)
 40+xs_str *xs_html_encode(const char *str)
 41 /* encodes str using HTML entities */
 42 {
 43     xs_str *s = xs_str_new(NULL);
 44     int o = 0;
 45-    char *e = str + strlen(str);
 46+    const char *e = str + strlen(str);
 47 
 48     for (;;) {
 49         char *ec = "<>\"'&";   /* characters to escape */
 50-        char *q = e;
 51+        const char *q = e;
 52         int z;
 53 
 54         /* find the nearest happening of a char */
 55@@ -90,7 +90,7 @@ xs_str *xs_html_encode(char *str)
 56 
 57 #define XS_HTML_NEW() memset(xs_realloc(NULL, sizeof(xs_html)), '\0', sizeof(xs_html))
 58 
 59-xs_html *xs_html_attr(char *key, char *value)
 60+xs_html *xs_html_attr(const char *key, const char *value)
 61 /* creates an HTML block with an attribute */
 62 {
 63     xs_html *a = XS_HTML_NEW();
 64@@ -108,7 +108,7 @@ xs_html *xs_html_attr(char *key, char *value)
 65 }
 66 
 67 
 68-xs_html *xs_html_text(char *content)
 69+xs_html *xs_html_text(const char *content)
 70 /* creates an HTML block of text, escaping it previously */
 71 {
 72     xs_html *a = XS_HTML_NEW();
 73@@ -120,7 +120,7 @@ xs_html *xs_html_text(char *content)
 74 }
 75 
 76 
 77-xs_html *xs_html_raw(char *content)
 78+xs_html *xs_html_raw(const char *content)
 79 /* creates an HTML block without escaping (for pre-formatted HTML, comments, etc) */
 80 {
 81     xs_html *a = XS_HTML_NEW();
 82@@ -152,7 +152,7 @@ xs_html *_xs_html_add(xs_html *tag, xs_html *var[])
 83 }
 84 
 85 
 86-static xs_html *_xs_html_tag_t(xs_html_type type, char *tag, xs_html *var[])
 87+static xs_html *_xs_html_tag_t(xs_html_type type, const char *tag, xs_html *var[])
 88 /* creates a tag with a variable list of attributes and subtags */
 89 {
 90     xs_html *a = XS_HTML_NEW();
 91@@ -169,13 +169,13 @@ static xs_html *_xs_html_tag_t(xs_html_type type, char *tag, xs_html *var[])
 92 }
 93 
 94 
 95-xs_html *_xs_html_tag(char *tag, xs_html *var[])
 96+xs_html *_xs_html_tag(const char *tag, xs_html *var[])
 97 {
 98     return _xs_html_tag_t(XS_HTML_TAG, tag, var);
 99 }
100 
101 
102-xs_html *_xs_html_sctag(char *tag, xs_html *var[])
103+xs_html *_xs_html_sctag(const char *tag, xs_html *var[])
104 {
105     return _xs_html_tag_t(XS_HTML_SCTAG, tag, var);
106 }
107@@ -239,7 +239,7 @@ void xs_html_render_f(xs_html *h, FILE *f)
108 }
109 
110 
111-xs_str *xs_html_render_s(xs_html *tag, char *prefix)
112+xs_str *xs_html_render_s(xs_html *tag, const char *prefix)
113 /* renders to a string */
114 {
115     xs_str *s = NULL;
+5, -4
 1@@ -16,7 +16,7 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size)
 2     xs *q_vars = NULL;
 3     xs *p_vars = NULL;
 4     xs *l1, *l2;
 5-    char *v;
 6+    const char *v;
 7 
 8     xs_socket_timeout(fileno(f), 2.0, 0.0);
 9 
10@@ -60,7 +60,8 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size)
11         p = xs_split_n(l, ": ", 1);
12 
13         if (xs_list_len(p) == 2)
14-            req = xs_dict_append(req, xs_tolower_i(xs_list_get(p, 0)), xs_list_get(p, 1));
15+            req = xs_dict_append(req, xs_tolower_i(
16+                    (xs_str *)xs_list_get(p, 0)), xs_list_get(p, 1));
17     }
18 
19     xs_socket_timeout(fileno(f), 5.0, 0.0);
20@@ -98,8 +99,8 @@ void xs_httpd_response(FILE *f, int status, xs_dict *headers, xs_str *body, int
21 /* sends an httpd response */
22 {
23     xs *proto;
24-    xs_str *k;
25-    xs_val *v;
26+    const xs_str *k;
27+    const xs_val *v;
28 
29     proto = xs_fmt("HTTP/1.1 %d %s", status, status / 100 == 2 ? "OK" : "ERROR");
30     fprintf(f, "%s\r\n", proto);
+11, -12
 1@@ -71,12 +71,12 @@ static void _xs_json_indent(int level, int indent, FILE *f)
 2 }
 3 
 4 
 5-static void _xs_json_dump(const xs_val *s_data, int level, int indent, FILE *f)
 6+static void _xs_json_dump(const xs_val *data, int level, int indent, FILE *f)
 7 /* dumps partial data as JSON */
 8 {
 9     int c = 0;
10-    xs_val *v;
11-    xs_val *data = (xs_val *)s_data;
12+    int ct = 0;
13+    const xs_val *v;
14 
15     switch (xs_type(data)) {
16     case XSTYPE_NULL:
17@@ -98,7 +98,7 @@ static void _xs_json_dump(const xs_val *s_data, int level, int indent, FILE *f)
18     case XSTYPE_LIST:
19         fputc('[', f);
20 
21-        while (xs_list_iter(&data, &v)) {
22+        while (xs_list_next(data, &v, &ct)) {
23             if (c != 0)
24                 fputc(',', f);
25 
26@@ -116,10 +116,9 @@ static void _xs_json_dump(const xs_val *s_data, int level, int indent, FILE *f)
27     case XSTYPE_DICT:
28         fputc('{', f);
29 
30-        xs_str *k;
31-        int ct = 0;
32+        const xs_str *k;
33 
34-        while (xs_dict_next(s_data, &k, &v, &ct)) {
35+        while (xs_dict_next(data, &k, &v, &ct)) {
36             if (c != 0)
37                 fputc(',', f);
38 
39@@ -328,7 +327,7 @@ static xs_val *_xs_json_load_lexer(FILE *f, js_type *t)
40 
41 int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c)
42 /* loads the next scalar value from the JSON stream */
43-/* if the value ahead is complex, value is NULL and pt is filled */
44+/* if the value ahead is compound, value is NULL and pt is set */
45 {
46     js_type t;
47 
48@@ -348,7 +347,7 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c)
49     }
50 
51     if (*value == NULL) {
52-        /* possible complex type ahead */
53+        /* possible compound type ahead */
54         if (t == JS_OBRACK)
55             *pt = XSTYPE_LIST;
56         else
57@@ -365,7 +364,7 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c)
58 
59 
60 xs_list *xs_json_load_array(FILE *f)
61-/* loads a JSON array (after the initial OBRACK) */
62+/* loads a full JSON array (after the initial OBRACK) */
63 {
64     xstype t;
65     xs_list *l = xs_list_new();
66@@ -406,7 +405,7 @@ xs_list *xs_json_load_array(FILE *f)
67 
68 int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c)
69 /* loads the next key and scalar value from the JSON stream */
70-/* if the value ahead is complex, value is NULL and pt is filled */
71+/* if the value ahead is compound, value is NULL and pt is set */
72 {
73     js_type t;
74 
75@@ -453,7 +452,7 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt,
76 
77 
78 xs_dict *xs_json_load_object(FILE *f)
79-/* loads a JSON object (after the initial OCURLY) */
80+/* loads a full JSON object (after the initial OCURLY) */
81 {
82     xstype t;
83     xs_dict *d = xs_dict_new();
+13, -9
 1@@ -55,19 +55,23 @@ const char *xs_mime_by_ext(const char *file)
 2     const char *ext = strrchr(file, '.');
 3 
 4     if (ext) {
 5-        const char **p = xs_mime_types;
 6-        xs *uext       = xs_tolower_i(xs_dup(ext + 1));
 7+        xs *uext = xs_tolower_i(xs_dup(ext + 1));
 8+        int b = 0;
 9+        int t = xs_countof(xs_mime_types) / 2 - 2;
10 
11-        while (*p) {
12-            int c;
13+        while (t >= b) {
14+            int n = (b + t) / 2;
15+            const char *p = xs_mime_types[n * 2];
16 
17-            if ((c = strcmp(*p, uext)) == 0)
18-                return p[1];
19+            int c = strcmp(uext, p);
20+
21+            if (c < 0)
22+                t = n - 1;
23             else
24             if (c > 0)
25-                break;
26-
27-            p += 2;
28+                b = n + 1;
29+            else
30+                return xs_mime_types[(n * 2) + 1];
31         }
32     }
33 
+30, -13
 1@@ -4,6 +4,7 @@
 2 
 3 #define _XS_REGEX_H
 4 
 5+int xs_regex_match(const char *str, const char *rx);
 6 xs_list *xs_regex_split_n(const char *str, const char *rx, int count);
 7 #define xs_regex_split(str, rx) xs_regex_split_n(str, rx, XS_ALL)
 8 xs_list *xs_regex_select_n(const char *str, const char *rx, int count);
 9@@ -15,21 +16,29 @@ xs_list *xs_regex_replace_in(xs_str *str, const char *rx, const char *rep, int c
10 
11 #ifdef XS_IMPLEMENTATION
12 
13+#ifdef __TINYC__
14+/* fix a compilation error in tcc */
15+#define _REGEX_NELTS(n)
16+#endif
17+
18 #include <regex.h>
19 
20 xs_list *xs_regex_split_n(const char *str, const char *rx, int count)
21-/* splits str by regex */
22+/* splits str using regex as a separator, at most count times.
23+    Always returns a list:
24+    len == 0: regcomp error
25+    len == 1: full string (no matches)
26+    len == odd: first part [ separator / next part ]...
27+*/
28 {
29     regex_t re;
30     regmatch_t rm;
31     int offset = 0;
32-    xs_list *list = NULL;
33+    xs_list *list = xs_list_new();
34     const char *p;
35 
36     if (regcomp(&re, rx, REG_EXTENDED))
37-        return NULL;
38-
39-    list = xs_list_new();
40+        return list;
41 
42     while (count > 0 && !regexec(&re, (p = str + offset), 1, &rm, offset > 0 ? REG_NOTBOL : 0)) {
43         /* add first the leading part of the string */
44@@ -60,16 +69,15 @@ xs_list *xs_regex_select_n(const char *str, const char *rx, int count)
45 {
46     xs_list *list = xs_list_new();
47     xs *split = NULL;
48-    xs_list *p;
49-    xs_val *v;
50+    const xs_val *v;
51     int n = 0;
52+    int c = 0;
53 
54     /* split */
55     split = xs_regex_split_n(str, rx, count);
56 
57     /* now iterate to get only the 'separators' (odd ones) */
58-    p = split;
59-    while (xs_list_iter(&p, &v)) {
60+    while (xs_list_next(split, &v, &c)) {
61         if (n & 0x1)
62             list = xs_list_append(list, v);
63 
64@@ -86,13 +94,12 @@ xs_list *xs_regex_replace_in(xs_str *str, const char *rx, const char *rep, int c
65 {
66     xs_str *s = xs_str_new(NULL);
67     xs *split = xs_regex_split_n(str, rx, count);
68-    xs_list *p;
69-    xs_val *v;
70+    const xs_val *v;
71     int n = 0;
72+    int c = 0;
73     int pholder = !!strchr(rep, '&');
74 
75-    p = split;
76-    while (xs_list_iter(&p, &v)) {
77+    while (xs_list_next(split, &v, &c)) {
78         if (n & 0x1) {
79             if (pholder) {
80                 /* rep has a placeholder; process char by char */
81@@ -128,6 +135,16 @@ xs_list *xs_regex_replace_in(xs_str *str, const char *rx, const char *rep, int c
82     return s;
83 }
84 
85+
86+int xs_regex_match(const char *str, const char *rx)
87+/* returns if str matches the regex at least once */
88+{
89+    xs *l = xs_regex_select_n(str, rx, 1);
90+
91+    return xs_list_len(l) == 1;
92+}
93+
94+
95 #endif /* XS_IMPLEMENTATION */
96 
97 #endif /* XS_REGEX_H */
+4, -4
 1@@ -85,7 +85,7 @@ int xs_set_add(xs_set *s, const xs_val *data)
 2 {
 3     /* is it 'full'? */
 4     if (s->used >= s->elems / 2) {
 5-        char *p, *v;
 6+        const xs_val *v;
 7 
 8         /* expand! */
 9         s->elems *= 2;
10@@ -95,8 +95,8 @@ int xs_set_add(xs_set *s, const xs_val *data)
11         memset(s->hash, '\0', s->elems * sizeof(int));
12 
13         /* add the list elements back */
14-        p = s->list;
15-        while (xs_list_iter(&p, &v))
16+        int ct = 0;
17+        while (xs_list_next(s->list, &v, &ct))
18             _store_hash(s, v, v - s->list);
19     }
20 
21@@ -104,7 +104,7 @@ int xs_set_add(xs_set *s, const xs_val *data)
22 
23     /* if it's new, add the data */
24     if (ret)
25-        s->list = xs_list_append_m(s->list, data, xs_size(data));
26+        s->list = xs_list_append(s->list, data);
27 
28     return ret;
29 }
+11, -11
 1@@ -6,7 +6,7 @@
 2 
 3  int _xs_utf8_enc(char buf[4], unsigned int cpoint);
 4  int xs_is_utf8_cont_byte(char c);
 5- unsigned int xs_utf8_dec(char **str);
 6+ unsigned int xs_utf8_dec(const char **str);
 7  int xs_unicode_width(unsigned int cpoint);
 8  int xs_is_surrogate(unsigned int cpoint);
 9  unsigned int xs_surrogate_dec(unsigned int p1, unsigned int p2);
10@@ -27,8 +27,8 @@
11 
12 #ifdef XS_IMPLEMENTATION
13 
14-#ifndef countof
15-#define countof(a) (sizeof((a)) / sizeof((*a)))
16+#ifndef xs_countof
17+#define xs_countof(a) (sizeof((a)) / sizeof((*a)))
18 #endif
19 
20 int _xs_utf8_enc(char buf[4], unsigned int cpoint)
21@@ -66,10 +66,10 @@ int xs_is_utf8_cont_byte(char c)
22 }
23 
24 
25-unsigned int xs_utf8_dec(char **str)
26+unsigned int xs_utf8_dec(const char **str)
27 /* decodes an utf-8 char inside str and updates the pointer */
28 {
29-    char *p = *str;
30+    const char *p = *str;
31     unsigned int cpoint = 0;
32     unsigned char c = *p++;
33     int cb = 0;
34@@ -125,7 +125,7 @@ int xs_unicode_width(unsigned int cpoint)
35 /* returns the width in columns of a Unicode codepoint (somewhat simplified) */
36 {
37     int b = 0;
38-    int t = countof(xs_unicode_width_table) / 3 - 1;
39+    int t = xs_countof(xs_unicode_width_table) / 3 - 1;
40 
41     while (t >= b) {
42         int n = (b + t) / 2;
43@@ -193,7 +193,7 @@ unsigned int *_xs_unicode_upper_search(unsigned int cpoint)
44 /* searches for an uppercase codepoint in the case fold table */
45 {
46     int b = 0;
47-    int t = countof(xs_unicode_case_fold_table) / 2 + 1;
48+    int t = xs_countof(xs_unicode_case_fold_table) / 2 + 1;
49 
50     while (t >= b) {
51         int n = (b + t) / 2;
52@@ -216,7 +216,7 @@ unsigned int *_xs_unicode_lower_search(unsigned int cpoint)
53 /* searches for a lowercase codepoint in the case fold table */
54 {
55     unsigned int *p = xs_unicode_case_fold_table;
56-    unsigned int *e = p + countof(xs_unicode_case_fold_table);
57+    unsigned int *e = p + xs_countof(xs_unicode_case_fold_table);
58 
59     while (p < e) {
60         if (cpoint == p[1])
61@@ -251,7 +251,7 @@ int xs_unicode_nfd(unsigned int cpoint, unsigned int *base, unsigned int *diac)
62 /* applies unicode Normalization Form D */
63 {
64     int b = 0;
65-    int t = countof(xs_unicode_nfd_table) / 3 - 1;
66+    int t = xs_countof(xs_unicode_nfd_table) / 3 - 1;
67 
68     while (t >= b) {
69         int n = (b + t) / 2;
70@@ -279,7 +279,7 @@ int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint)
71 /* applies unicode Normalization Form C */
72 {
73     unsigned int *p = xs_unicode_nfd_table;
74-    unsigned int *e = p + countof(xs_unicode_nfd_table);
75+    unsigned int *e = p + xs_countof(xs_unicode_nfd_table);
76 
77     while (p < e) {
78         if (p[1] == base && p[2] == diac) {
79@@ -298,7 +298,7 @@ int xs_unicode_is_alpha(unsigned int cpoint)
80 /* checks if a codepoint is an alpha (i.e. a letter) */
81 {
82     int b = 0;
83-    int t = countof(xs_unicode_alpha_table) / 2 - 1;
84+    int t = xs_countof(xs_unicode_alpha_table) / 2 - 1;
85 
86     while (t >= b) {
87         int n = (b + t) / 2;
+6, -7
 1@@ -51,12 +51,11 @@ xs_dict *xs_url_vars(const char *str)
 2         /* split by arguments */
 3         xs *args = xs_split(str, "&");
 4 
 5-        xs_list *l;
 6-        xs_val *v;
 7+        int ct = 0;
 8+        const xs_val *v;
 9 
10-        l = args;
11-        while (xs_list_iter(&l, &v)) {
12-            xs *kv = xs_split_n(v, "=", 2);
13+        while (xs_list_next(args, &v, &ct)) {
14+            xs *kv = xs_split_n(v, "=", 1);
15 
16             if (xs_list_len(kv) == 2) {
17                 const char *key = xs_list_get(kv, 0);
18@@ -119,8 +118,8 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
19     while ((p = xs_memmem(payload + offset, p_size - offset, boundary, bsz)) != NULL) {
20         xs *s1 = NULL;
21         xs *l1 = NULL;
22-        char *vn = NULL;
23-        char *fn = NULL;
24+        const char *vn = NULL;
25+        const char *fn = NULL;
26         char *q;
27         int po, ps;
28 
+1, -1
1@@ -1 +1 @@
2-/* 0df383371d207b0adfda40912ee54b37e5b01152 2024-03-15T03:45:39+01:00 */
3+/* 65769f25ed99b886a643522bef21628396cd118d 2024-05-25T08:18:51+02:00 */