commit 84a767d
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
Makefile
+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).
M
TODO.md
+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.
M
format.c
+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(" 📅 ")));
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')
M
httpd.c
+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
M
utils.c
+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 */
M
xs_set.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;
M
xs_url.h
+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 */