snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

commit 7611a6bee4bcbad2f1710aafa99aba730e5cf995
parent 3d96c576287736ebdf87e4a7b956842a6c6e055f
Author: shtrophic <christoph@liebender.dev>
Date:   Sat, 15 Feb 2025 14:37:36 +0100

Merge tag '2.72' into curl-smtp

Version 2.72 RELEASED.

Diffstat:
MREADME.md | 2++
MRELEASE_NOTES.md | 38++++++++++++++++++++++++++++++++++++++
MTODO.md | 8+++++---
Mactivitypub.c | 2+-
Mdata.c | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mdoc/snac.1 | 5+++++
Mdoc/snac.5 | 2+-
Mdoc/snac.8 | 6++++++
Mdoc/style.css | 1+
Mformat.c | 6++++--
Mhtml.c | 393++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mhttpd.c | 3++-
Mmain.c | 49+++++++++++++++++++++++++++++++++++++------------
Mmastoapi.c | 9+++++++--
Msnac.c | 2+-
Msnac.h | 10+++++++---
Mutils.c | 3+++
Mxs.h | 36++++++++++++++++++++++++++++--------
Mxs_fcgi.h | 3+++
Mxs_html.h | 4++--
Mxs_httpd.h | 54++++++++++++++++++++++++++++++++++--------------------
Mxs_io.h | 7+++----
Mxs_json.h | 6++++++
Mxs_match.h | 7++++++-
Mxs_openssl.h | 2+-
Mxs_socket.h | 2++
Mxs_url.h | 117++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mxs_version.h | 2+-
28 files changed, 636 insertions(+), 225 deletions(-)

diff --git a/README.md b/README.md @@ -107,6 +107,8 @@ This will: - [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html). - [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac). - [How to run your own social network with snac (by Giacomo Tesio)](https://encrypted.tesio.it/2024/12/18/how-to-run-your-own-social-network.html). Includes information on how to run snac as a CGI. +- [Improving snac Performance with Nginx Proxy Cache (by Stefano Marinelli)](https://it-notes.dragas.net/2025/01/29/improving-snac-performance-with-nginx-proxy-cache/). +- [Caching Snac Proxied Media With Nginx (by Stefano Marinelli)](https://it-notes.dragas.net/2025/02/08/caching-snac-proxied-media-with-nginx/). ## Incredibly awesome CSS themes for snac diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md @@ -1,5 +1,43 @@ # Release Notes +## 2.72 + +Each post can have more than one attachment from the web UI. The maximum number can be configured in `server.json` via the `max_attachments` value (default: 4). + +Each notification includes a link labelled `Context`, that leads to a page with the full conversation tree the post is a part of. + +Each followed hashtag has now a directly accesible link. + +Fixed a search bug (some matches were missed). + +Fixed more crashes (contributed by inz). + +Fixed link detection in posts (contributed by inz). + +Allow multiple editors for command-line posts (contributed by inz). + +Separated maximum and default timeline entry count, allowing larger timelines to be requested without having to increase the default (contributed by lxo). + +Turned message date into a link to the local post, so that it can be loaded into a separate tab for interacting with (contributed by lxo). + +Special thanks to fellow developer inz for bringing my attention to code places where I should have been more careful. + +## 2.71 + +Fixed memory leak (contributed by inz). + +Fixed crash. + +## 2.70 + +Notifications are now shown in a more compact way (i.e. all reactions are shown just above your post, instead of repeating the post *ad nauseam* for every reaction). + +New command-line option `unmute` to, well, no-longer-mute an actor. + +The private timeline now includes an approximate mark between new posts and "already seen" ones. + +Fixed a spurious 404 error in the instance root URL for some configurations. + ## 2.69 "Yin/Yang of Love" Added support for subscribing to LitePub (Pleroma-style) Fediverse Relays like e.g. https://fedi-relay.gyptazy.com to improve federation. See `snac(8)` (the Administrator Manual) for more information on how to use this feature. diff --git a/TODO.md b/TODO.md @@ -14,14 +14,12 @@ Important: deleting a follower should do more that just delete the object, see h ## Wishlist -Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information). +Each notification should show a link to the full thread, to see it in context. The instance timeline should also show boosts from users. Mastoapi: implement /v1/conversations. -Implement following of hashtags (this is not trivial). - Track 'Event' data types standardization; how to add plan-to-attend and similar activities (more info: https://event-federation.eu/). Friendica interacts with events via activities `Accept` (will go), `TentativeAccept` (will try to go) or `Reject` (cannot go) (`object` field as id, not object). `Undo` for any of these activities cancel (`object` as an object, not id). Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md @@ -365,3 +363,7 @@ CSV import/export does not work with OpenBSD security on; document it or fix it Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100). Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100). + +Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100). + +Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information) (2025-01-30T16:12:34+0100). diff --git a/activitypub.c b/activitypub.c @@ -3072,7 +3072,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path, int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); /* get the public outbox or the pinned list */ - xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt) : pinned_list(&snac); + xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt, NULL) : pinned_list(&snac); xs_list_foreach(elems, v) { xs *i = NULL; diff --git a/data.c b/data.c @@ -1399,11 +1399,13 @@ void timeline_update_indexes(snac *snac, const char *id) if (valid_status(object_get(id, &msg))) { /* if its ours and is public, also store in public */ if (is_msg_public(msg)) { - object_user_cache_add(snac, id, "public"); - - /* also add it to the instance public timeline */ - xs *ipt = xs_fmt("%s/public.idx", srv_basedir); - index_add(ipt, id); + if (object_user_cache_add(snac, id, "public") >= 0) { + /* also add it to the instance public timeline */ + xs *ipt = xs_fmt("%s/public.idx", srv_basedir); + index_add(ipt, id); + } + else + srv_debug(1, xs_fmt("Not added to public instance index %s", id)); } } } @@ -1487,16 +1489,28 @@ xs_str *user_index_fn(snac *user, const char *idx_name) } -xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show) +xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more) /* returns a timeline (with all entries) */ { xs *idx = user_index_fn(user, idx_name); - return index_list_desc(idx, skip, show); + /* if a more flag is sent, request one more */ + xs_list *lst = index_list_desc(idx, skip, show + (more != NULL ? 1 : 0)); + + if (more != NULL) { + if (xs_list_len(lst) > show) { + *more = 1; + lst = xs_list_del(lst, -1); + } + else + *more = 0; + } + + return lst; } -xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) +xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more) /* returns a timeline (only top level entries) */ { int c_max; @@ -1508,12 +1522,33 @@ xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) if (show > c_max) show = c_max; - xs *list = timeline_simple_list(snac, idx_name, skip, show); + xs *list = timeline_simple_list(snac, idx_name, skip, show, more); return timeline_top_level(snac, list); } +void timeline_add_mark(snac *user) +/* adds an "already seen" mark to the private timeline */ +{ + xs *fn = xs_fmt("%s/private.idx", user->basedir); + char last_entry[MD5_HEX_SIZE] = ""; + FILE *f; + + /* get the last entry in the index */ + if ((f = fopen(fn, "r")) != NULL) { + index_desc_first(f, last_entry, 0); + fclose(f); + } + + /* is the last entry *not* a mark? */ + if (strcmp(last_entry, MD5_ALREADY_SEEN_MARK) != 0) { + /* add it */ + index_add_md5(fn, MD5_ALREADY_SEEN_MARK); + } +} + + xs_str *instance_index_fn(void) { return xs_fmt("%s/public.idx", srv_basedir); @@ -1524,8 +1559,17 @@ xs_list *timeline_instance_list(int skip, int show) /* returns the timeline for the full instance */ { xs *idx = instance_index_fn(); + xs *lst = index_list_desc(idx, skip, show); - return index_list_desc(idx, skip, show); + /* make the list unique */ + xs_set rep; + xs_set_init(&rep); + const char *md5; + + xs_list_foreach(lst, md5) + xs_set_add(&rep, md5); + + return xs_set_result(&rep); } @@ -2557,7 +2601,7 @@ xs_list *inbox_list(void) if ((f = fopen(v, "r")) != NULL) { xs *line = xs_readline(f); - if (line) { + if (line && *line) { line = xs_strip_i(line); ibl = xs_list_append(ibl, line); } @@ -2698,9 +2742,9 @@ xs_list *content_search(snac *user, const char *regex, const char *md5s[3] = {0}; int c[3] = {0}; - tls[0] = timeline_simple_list(user, "public", 0, XS_ALL); /* public */ + tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL); /* public */ tls[1] = timeline_instance_list(0, XS_ALL); /* instance */ - tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL) : xs_list_new(); /* private or none */ + tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */ /* first positioning */ for (int n = 0; n < 3; n++) @@ -2722,7 +2766,17 @@ xs_list *content_search(snac *user, const char *regex, for (int n = 0; n < 3; n++) { if (md5s[n] != NULL) { xs *fn = _object_fn_by_md5(md5s[n], "content_search"); - double mt = mtime(fn); + double mt; + + while ((mt = mtime(fn)) == 0 && md5s[n] != NULL) { + /* object is not here: move to the next one */ + if (xs_list_next(tls[n], &md5s[n], &c[n])) { + xs_free(fn); + fn = _object_fn_by_md5(md5s[n], "content_search_2"); + } + else + md5s[n] = NULL; + } if (mt > mtime) { newest = n; diff --git a/doc/snac.1 b/doc/snac.1 @@ -234,6 +234,8 @@ Purges old data from the timeline of all users. .It Cm adduser Ar basedir Op uid Adds a new user to the server. This is an interactive command; necessary information will be prompted for. +.It Cm deluser Ar basedir Ar uid +Deletes a user, unfollowing all accounts first. .It Cm resetpwd Ar basedir Ar uid Resets a user's password to a new, random one. .It Cm queue Ar basedir Ar uid @@ -257,6 +259,9 @@ The rest of command line arguments are treated as media files to be attached to the post. .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... Like the previous one, but creates an "unlisted" (or "quiet public") post. +.It Cm note_mention Ar basedir Ar uid Ar text Op file file ... +Like the previous one, but creates a post only for accounts mentioned +in the post body. .It Cm block Ar basedir Ar instance_url Blocks a full instance, given its URL or domain name. All subsequent incoming activities with identifiers from that instance will be immediately diff --git a/doc/snac.5 b/doc/snac.5 @@ -78,7 +78,7 @@ converted to related emojis: .Ss Accepted HTML All HTML tags in entries are neutered except the following ones: .Bd -literal -a p br blockquote ul ol li cite small +a p br blockquote ul ol li cite small h2 h3 span i b u s pre code em strong hr img del .Ed .Pp diff --git a/doc/snac.8 b/doc/snac.8 @@ -154,6 +154,8 @@ to those servers that went timeout in the previous retry. If you want to give slow servers a chance to receive your messages, you can increase this value (but also take into account that processing the queue will take longer while waiting for these molasses to respond). +.It Ic def_timeline_entries +This is the default timeline entries shown in the web interface. .It Ic max_timeline_entries This is the maximum timeline entries shown in the web interface. .It Ic timeline_purge_days @@ -205,6 +207,8 @@ The email address of the instance administrator (optional). The user name of the instance administrator (optional). .It Ic short_description A textual short description about the instance (optional). +.It Ic short_description_raw +Whether to interpret short_descript as raw string or convert to HTML (optional). .It Ic fastcgi If set to true, .Nm @@ -256,6 +260,8 @@ need at least a Linux kernel version 5.13.0. .It Ic max_public_entries The maximum number of entries (posts) to be returned in user RSS feeds and outboxes (default: 20). +.It Ic max_attachments +The maximum number of attachments per post (default: 4). .El .Pp You must restart the server to make effective these changes. diff --git a/doc/style.css b/doc/style.css @@ -29,6 +29,7 @@ pre { overflow-x: scroll; } .snac-list-of-lists { padding-left: 0; } .snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px; margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; } +.snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; } @media (prefers-color-scheme: dark) { body, input, textarea { background-color: #000; color: #fff; } a { color: #7799dd } diff --git a/format.c b/format.c @@ -78,6 +78,8 @@ xs_dict *emojis(void) return d; } +/* Non-whitespace without trailing comma, period or closing paren */ +#define NOSPACE "([^[:space:],.)]+|[,.)]+[^[:space:],.)])+" static xs_str *format_line(const char *line, xs_list **attach) /* formats a line */ @@ -96,8 +98,8 @@ static xs_str *format_line(const char *line, xs_list **attach) "__[^_]+__" "|" //anzu "!\\[[^]]+\\]\\([^\\)]+\\)" "|" "\\[[^]]+\\]\\([^\\)]+\\)" "|" - "[a-z]+:/" "/[^[:space:]]+" "|" - "(mailto|xmpp):[^@[:space:]]+@[^[:space:]]+" + "[a-z]+:/" "/" NOSPACE "|" + "(mailto|xmpp):[^@[:space:]]+@" NOSPACE ")"); int n = 0; diff --git a/html.c b/html.c @@ -13,6 +13,7 @@ #include "xs_html.h" #include "xs_curl.h" #include "xs_unicode.h" +#include "xs_url.h" #include "snac.h" @@ -115,7 +116,8 @@ xs_str *actor_name(xs_dict *actor, const char *proxy) xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, const char *udate, const char *url, int priv, - int in_people, const char *proxy, const char *lang) + int in_people, const char *proxy, const char *lang, + const char *md5) { xs_html *actor_icon = xs_html_tag("p", NULL); @@ -224,12 +226,31 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, if (xs_is_string(lang)) date_title = xs_str_cat(date_title, " (", lang, ")"); + xs_html *date_text = xs_html_text(date_label); + + if (user && md5) { + xs *lpost_url = xs_fmt("%s/admin/p/%s#%s_entry", + user->actor, md5, md5); + date_text = xs_html_tag("a", + xs_html_attr("href", lpost_url), + xs_html_attr("class", "snac-pubdate"), + date_text); + } + else if (user && url) { + xs *lpost_url = xs_fmt("%s/admin?q=%s", + user->actor, xs_url_enc(url)); + date_text = xs_html_tag("a", + xs_html_attr("href", lpost_url), + xs_html_attr("class", "snac-pubdate"), + date_text); + } + xs_html_add(actor_icon, xs_html_text(" "), xs_html_tag("time", xs_html_attr("class", "dt-published snac-pubdate"), xs_html_attr("title", date_title), - xs_html_text(date_label))); + date_text)); } { @@ -261,7 +282,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, } -xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, const char *proxy) +xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, const char *proxy, const char *md5) { xs *actor = NULL; xs_html *actor_icon = NULL; @@ -292,7 +313,7 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, con else lang = NULL; - actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy, lang); + actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy, lang, md5); } return actor_icon; @@ -306,7 +327,7 @@ xs_html *html_note(snac *user, const char *summary, const xs_val *cw_yn, const char *cw_text, const xs_val *mnt_only, const char *redir, const char *in_reply_to, int poll, - const char *att_file, const char *att_alt_text, + const xs_list *att_files, const xs_list *att_alt_texts, int is_draft) /* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */ { @@ -411,30 +432,71 @@ xs_html *html_note(snac *user, const char *summary, xs_html_tag("p", NULL), att = xs_html_tag("details", xs_html_tag("summary", - xs_html_text(L("Attachment..."))), + xs_html_text(L("Attachments..."))), xs_html_tag("p", NULL))); - if (att_file && *att_file) + int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4")); + int att_n = 0; + + /* fields for the currently existing attachments */ + if (xs_is_list(att_files) && xs_is_list(att_alt_texts)) { + while (att_n < max_attachments) { + const char *att_file = xs_list_get(att_files, att_n); + const char *att_alt_text = xs_list_get(att_alt_texts, att_n); + + if (!xs_is_string(att_file) || !xs_is_string(att_alt_text)) + break; + + xs *att_lbl = xs_fmt("attach_url_%d", att_n); + xs *alt_lbl = xs_fmt("alt_text_%d", att_n); + + if (att_n) + xs_html_add(att, + xs_html_sctag("br", NULL)); + + xs_html_add(att, + xs_html_text(L("File:")), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", att_lbl), + xs_html_attr("title", L("Clear this field to delete the attachment")), + xs_html_attr("value", att_file))); + + xs_html_add(att, + xs_html_text(" "), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", alt_lbl), + xs_html_attr("value", att_alt_text), + xs_html_attr("placeholder", L("Attachment description")))); + + att_n++; + } + } + + /* the rest of possible attachments */ + while (att_n < max_attachments) { + xs *att_lbl = xs_fmt("attach_%d", att_n); + xs *alt_lbl = xs_fmt("alt_text_%d", att_n); + + if (att_n) + xs_html_add(att, + xs_html_sctag("br", NULL)); + xs_html_add(att, - xs_html_text(L("File:")), xs_html_sctag("input", - xs_html_attr("type", "text"), - xs_html_attr("name", "attach_url"), - xs_html_attr("title", L("Clear this field to delete the attachment")), - xs_html_attr("value", att_file))); - else + xs_html_attr("type", "file"), + xs_html_attr("name", att_lbl))); + xs_html_add(att, + xs_html_text(" "), xs_html_sctag("input", - xs_html_attr("type", "file"), - xs_html_attr("name", "attach"))); + xs_html_attr("type", "text"), + xs_html_attr("name", alt_lbl), + xs_html_attr("placeholder", L("Attachment description")))); - xs_html_add(att, - xs_html_text(" "), - xs_html_sctag("input", - xs_html_attr("type", "text"), - xs_html_attr("name", "alt_text"), - xs_html_attr("value", att_alt_text), - xs_html_attr("placeholder", L("Attachment description")))); + att_n++; + } /* add poll controls */ if (poll) { @@ -553,10 +615,11 @@ xs_html *html_instance_head(void) static xs_html *html_instance_body(void) { - const char *host = xs_dict_get(srv_config, "host"); - const char *sdesc = xs_dict_get(srv_config, "short_description"); - const char *email = xs_dict_get(srv_config, "admin_email"); - const char *acct = xs_dict_get(srv_config, "admin_account"); + const char *host = xs_dict_get(srv_config, "host"); + const char *sdesc = xs_dict_get(srv_config, "short_description"); + const char *sdescraw = xs_dict_get(srv_config, "short_description_raw"); + const char *email = xs_dict_get(srv_config, "admin_email"); + const char *acct = xs_dict_get(srv_config, "admin_account"); xs *blurb = xs_replace(snac_blurb, "%host%", host); @@ -569,12 +632,21 @@ static xs_html *html_instance_body(void) dl = xs_html_tag("dl", NULL))); if (sdesc && *sdesc) { - xs_html_add(dl, - xs_html_tag("di", - xs_html_tag("dt", - xs_html_text(L("Site description"))), - xs_html_tag("dd", - xs_html_text(sdesc)))); + if (!xs_is_null(sdescraw) && xs_type(sdescraw) == XSTYPE_TRUE) { + xs_html_add(dl, + xs_html_tag("di", + xs_html_tag("dt", + xs_html_text(L("Site description"))), + xs_html_tag("dd", + xs_html_raw(sdesc)))); + } else { + xs_html_add(dl, + xs_html_tag("di", + xs_html_tag("dt", + xs_html_text(L("Site description"))), + xs_html_tag("dd", + xs_html_text(sdesc)))); + } } if (email && *email) { xs *mailto = xs_fmt("mailto:%s", email); @@ -1028,7 +1100,7 @@ xs_html *html_top_controls(snac *snac) NULL, NULL, xs_stock(XSTYPE_FALSE), "", xs_stock(XSTYPE_FALSE), NULL, - NULL, 1, "", "", 0), + NULL, 1, NULL, NULL, 0), /** operations **/ xs_html_tag("details", @@ -1600,17 +1672,22 @@ xs_html *html_entry_controls(snac *snac, const char *actor, xs *form_id = xs_fmt("%s_edit_form", md5); xs *redir = xs_fmt("%s_entry", md5); - const char *att_file = ""; - const char *att_alt_text = ""; + xs *att_files = xs_list_new(); + xs *att_alt_texts = xs_list_new(); + const xs_list *att_list = xs_dict_get(msg, "attachment"); - /* does it have an attachment? */ - if (xs_type(att_list) == XSTYPE_LIST && xs_list_len(att_list)) { - const xs_dict *d = xs_list_get(att_list, 0); + if (xs_is_list(att_list)) { + const xs_dict *d; + + xs_list_foreach(att_list, d) { + const char *att_file = xs_dict_get(d, "url"); + const char *att_alt_text = xs_dict_get(d, "name"); - if (xs_type(d) == XSTYPE_DICT) { - att_file = xs_dict_get_def(d, "url", ""); - att_alt_text = xs_dict_get_def(d, "name", ""); + if (xs_is_string(att_file) && xs_is_string(att_alt_text)) { + att_files = xs_list_append(att_files, att_file); + att_alt_texts = xs_list_append(att_alt_texts, att_alt_text); + } } } @@ -1622,7 +1699,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor, id, NULL, xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, - NULL, 0, att_file, att_alt_text, is_draft(snac, id))), + NULL, 0, att_files, att_alt_texts, is_draft(snac, id))), xs_html_tag("p", NULL)); } @@ -1641,7 +1718,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor, NULL, NULL, xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, - id, 0, "", "", 0)), + id, 0, NULL, NULL, 0)), xs_html_tag("p", NULL)); } @@ -1696,7 +1773,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_tag("div", xs_html_attr("class", "snac-origin"), xs_html_text(L("follows you"))), - html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy))); + html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy, NULL))); } else if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) { @@ -1877,7 +1954,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, } xs_html_add(post_header, - html_msg_icon(read_only ? NULL : user, actor, msg, proxy)); + html_msg_icon(read_only ? NULL : user, actor, msg, proxy, md5)); /** post content **/ @@ -2022,16 +2099,17 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, const char *name = xs_dict_get(v, "name"); const xs_dict *replies = xs_dict_get(v, "replies"); - if (name && replies) { - char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems")); + if (xs_is_string(name) && xs_is_dict(replies)) { + const char *ti = xs_number_str(xs_dict_get(replies, "totalItems")); - xs_html_add(poll_result, - xs_html_tag("tr", - xs_html_tag("td", - xs_html_text(name), - xs_html_text(":")), - xs_html_tag("td", - xs_html_text(ti)))); + if (xs_is_string(ti)) + xs_html_add(poll_result, + xs_html_tag("tr", + xs_html_tag("td", + xs_html_text(name), + xs_html_text(":")), + xs_html_tag("td", + xs_html_text(ti)))); } } @@ -2629,6 +2707,29 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, xs_html_attr("title", L("Post drafts")), xs_html_text("drafts")))); } + + /* the list of followed hashtags */ + const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags"); + + if (xs_is_list(followed_hashtags) && xs_list_len(followed_hashtags)) { + xs_html *loht = xs_html_tag("ul", + xs_html_attr("class", "snac-list-of-lists")); + xs_html_add(body, loht); + + const char *ht; + + xs_list_foreach(followed_hashtags, ht) { + xs *url = xs_fmt("%s/admin?q=%s", user->actor, ht); + url = xs_replace_i(url, "#", "%23"); + + xs_html_add(loht, + xs_html_tag("li", + xs_html_tag("a", + xs_html_attr("href", url), + xs_html_attr("class", "snac-list-link"), + xs_html_text(ht)))); + } + } } xs_html_add(body, @@ -2648,10 +2749,32 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, xs_html_add(body, posts); + int mark_shown = 0; + while (xs_list_iter(&p, &v)) { xs *msg = NULL; int status; + /* "already seen" mark? */ + if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) { + if (skip == 0 && !mark_shown) { + xs *s = xs_fmt("%s/admin", user->actor); + + xs_html_add(posts, + xs_html_tag("div", + xs_html_attr("class", "snac-no-more-unseen-posts"), + xs_html_text(L("No more unseen posts")), + xs_html_text(" - "), + xs_html_tag("a", + xs_html_attr("href", s), + xs_html_text(L("Back to top"))))); + } + + mark_shown = 1; + + continue; + } + if (utl && user && !is_pinned_by_md5(user, v)) status = timeline_get_by_md5(user, v, &msg); else @@ -2788,7 +2911,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons xs_html_tag("div", xs_html_attr("class", "snac-post-header"), html_actor_icon(snac, actor, xs_dict_get(actor, "published"), - NULL, NULL, 0, 1, proxy, NULL))); + NULL, NULL, 0, 1, proxy, NULL, NULL))); /* content (user bio) */ const char *c = xs_dict_get(actor, "summary"); @@ -2885,7 +3008,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons NULL, actor_id, xs_stock(XSTYPE_FALSE), "", xs_stock(XSTYPE_FALSE), NULL, - NULL, 0, "", "", 0), + NULL, 0, NULL, NULL, 0), xs_html_tag("p", NULL)); xs_html_add(snac_post, snac_controls); @@ -2970,6 +3093,9 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_set rep; xs_set_init(&rep); + /* dict to store previous notification labels */ + xs *admiration_labels = xs_dict_new(); + const xs_str *v; xs_list_foreach(n_list, v) { @@ -2983,6 +3109,7 @@ xs_str *html_notifications(snac *user, int skip, int show) const char *utype = xs_dict_get(noti, "utype"); const char *id = xs_dict_get(noti, "objid"); const char *date = xs_dict_get(noti, "date"); + const char *id2 = xs_dict_get_path(noti, "msg.id"); xs *wrk = NULL; if (xs_is_null(id)) @@ -2991,12 +3118,15 @@ xs_str *html_notifications(snac *user, int skip, int show) if (is_hidden(user, id)) continue; + if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1) + continue; + object_get(id, &obj); const char *msg_id = NULL; - if (xs_is_dict(obj) && (msg_id = xs_dict_get(obj, "id")) && xs_set_add(&rep, msg_id) != 1) - continue; + if (xs_is_dict(obj)) + msg_id = xs_dict_get(obj, "id"); const char *actor_id = xs_dict_get(noti, "actor"); xs *actor = NULL; @@ -3030,9 +3160,7 @@ xs_str *html_notifications(snac *user, int skip, int show) xs *s_date = xs_crop_i(xs_dup(date), 0, 10); - xs_html *entry = xs_html_tag("div", - xs_html_attr("class", "snac-post-with-desc"), - xs_html_tag("p", + xs_html *this_html_label = xs_html_container( xs_html_tag("b", xs_html_text(label), xs_html_text(" by "), @@ -3043,13 +3171,45 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_tag("time", xs_html_attr("class", "dt-published snac-pubdate"), xs_html_attr("title", date), - xs_html_text(s_date)))); + xs_html_text(s_date))); + + xs_html *html_label = NULL; + + if (xs_is_string(msg_id)) { + const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id); + + if (xs_type(prev_label) == XSTYPE_DATA) { + /* there is a previous list of admiration labels! */ + xs_data_get(&html_label, prev_label); + + xs_html_add(html_label, + xs_html_sctag("br", NULL), + this_html_label); + + continue; + } + } + + xs_html *entry = NULL; + + html_label = xs_html_tag("p", + this_html_label); + + /* store in the admiration labels dict */ + xs *pl = xs_data_new(&html_label, sizeof(html_label)); + + if (xs_is_string(msg_id)) + admiration_labels = xs_dict_set(admiration_labels, msg_id, pl); + + entry = xs_html_tag("div", + xs_html_attr("class", "snac-post-with-desc"), + html_label); if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) { xs_html_add(entry, xs_html_tag("div", xs_html_attr("class", "snac-post"), - html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL))); + html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL))); } else if (strcmp(type, "Move") == 0) { @@ -3063,18 +3223,23 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_add(entry, xs_html_tag("div", xs_html_attr("class", "snac-post"), - html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy, NULL))); + html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL))); } } } else if (obj != NULL) { xs *md5 = xs_md5_hex(id, strlen(id)); + xs *ctxt = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5); xs_html *h = html_entry(user, obj, 0, 0, md5, 1); if (h != NULL) { xs_html_add(entry, + xs_html_tag("p", + xs_html_tag("a", + xs_html_attr("href", ctxt), + xs_html_text(L("Context")))), h); } } @@ -3111,8 +3276,6 @@ xs_str *html_notifications(snac *user, int skip, int show) } } - xs_set_free(&rep); - if (noti_new == NULL && noti_seen == NULL) xs_html_add(body, xs_html_tag("h2", @@ -3132,6 +3295,8 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_text(L("More..."))))); } + xs_set_free(&rep); + xs_html_add(body, html_footer()); @@ -3232,7 +3397,8 @@ int html_get_handler(const xs_dict *req, const char *q_path, cache = 0; int skip = 0; - int def_show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries")); + int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries", + xs_dict_get_def(srv_config, "max_timeline_entries", "50"))); int show = def_show; if ((v = xs_dict_get(q_vars, "skip")) != NULL) @@ -3277,21 +3443,17 @@ int html_get_handler(const xs_dict *req, const char *q_path, } else { xs *list = NULL; - xs *next = NULL; + int more = 0; - if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) { - list = timeline_simple_list(&snac, "public", skip, show); - next = timeline_simple_list(&snac, "public", skip + show, 1); - } - else { - list = timeline_list(&snac, "public", skip, show); - next = timeline_list(&snac, "public", skip + show, 1); - } + if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) + list = timeline_simple_list(&snac, "public", skip, show, &more); + else + list = timeline_list(&snac, "public", skip, show, &more); xs *pins = pinned_list(&snac); pins = xs_list_cat(pins, list); - *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1, error); + *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error); *b_size = strlen(*body); status = HTTP_STATUS_OK; @@ -3440,6 +3602,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, } } else { + /** the private timeline **/ double t = history_mtime(&snac, "timeline.html_"); /* if enabled by admin, return a cached page if its timestamp is: @@ -3453,19 +3616,22 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs_dict_get(req, "if-none-match"), etag); } else { + int more = 0; + snac_debug(&snac, 1, xs_fmt("building timeline")); - xs *list = timeline_list(&snac, "private", skip, show); - xs *next = timeline_list(&snac, "private", skip + show, 1); + xs *list = timeline_list(&snac, "private", skip, show, &more); *body = html_timeline(&snac, list, 0, skip, show, - xs_list_len(next), NULL, "/admin", 1, error); + more, NULL, "/admin", 1, error); *b_size = strlen(*body); status = HTTP_STATUS_OK; if (save) history_add(&snac, "timeline.html_", *body, *b_size, etag); + + timeline_add_mark(&snac); } } } @@ -3481,7 +3647,8 @@ int html_get_handler(const xs_dict *req, const char *q_path, const char *md5 = xs_list_get(l, -1); if (md5 && *md5 && timeline_here(&snac, md5)) { - xs *list = xs_list_append(xs_list_new(), md5); + xs *list0 = xs_list_append(xs_list_new(), md5); + xs *list = timeline_top_level(&snac, list0); *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error); *b_size = strlen(*body); @@ -3665,7 +3832,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); - xs *elems = timeline_simple_list(&snac, "public", 0, cnt); + xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL); xs *bio = xs_dup(xs_dict_get(snac.config, "bio")); xs *rss_title = xs_fmt("%s (@%s@%s)", @@ -3869,52 +4036,56 @@ int html_post_handler(const xs_dict *req, const char *q_path, /* post note */ const xs_str *content = xs_dict_get(p_vars, "content"); const xs_str *in_reply_to = xs_dict_get(p_vars, "in_reply_to"); - const xs_str *attach_url = xs_dict_get(p_vars, "attach_url"); - const xs_list *attach_file = xs_dict_get(p_vars, "attach"); const xs_str *to = xs_dict_get(p_vars, "to"); const xs_str *sensitive = xs_dict_get(p_vars, "sensitive"); const xs_str *summary = xs_dict_get(p_vars, "summary"); const xs_str *edit_id = xs_dict_get(p_vars, "edit_id"); - const xs_str *alt_text = xs_dict_get(p_vars, "alt_text"); int priv = !xs_is_null(xs_dict_get(p_vars, "mentioned_only")); int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft")); xs *attach_list = xs_list_new(); - /* default alt text */ - if (xs_is_null(alt_text)) - alt_text = ""; + /* iterate the attachments */ + int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4")); - /* is attach_url set? */ - if (!xs_is_null(attach_url) && *attach_url != '\0') { - xs *l = xs_list_new(); + for (int att_n = 0; att_n < max_attachments; att_n++) { + xs *url_lbl = xs_fmt("attach_url_%d", att_n); + xs *att_lbl = xs_fmt("attach_%d", att_n); + xs *alt_lbl = xs_fmt("alt_text_%d", att_n); - l = xs_list_append(l, attach_url); - l = xs_list_append(l, alt_text); + const char *attach_url = xs_dict_get(p_vars, url_lbl); + const xs_list *attach_file = xs_dict_get(p_vars, att_lbl); + const char *alt_text = xs_dict_get_def(p_vars, alt_lbl, ""); - attach_list = xs_list_append(attach_list, l); - } + if (xs_is_string(attach_url) && *attach_url != '\0') { + xs *l = xs_list_new(); - /* is attach_file set? */ - if (!xs_is_null(attach_file) && xs_type(attach_file) == XSTYPE_LIST) { - const char *fn = xs_list_get(attach_file, 0); + l = xs_list_append(l, attach_url); + l = xs_list_append(l, alt_text); - if (*fn != '\0') { - char *ext = strrchr(fn, '.'); - xs *hash = xs_md5_hex(fn, strlen(fn)); - xs *id = xs_fmt("%s%s", hash, ext); - xs *url = xs_fmt("%s/s/%s", snac.actor, id); - int fo = xs_number_get(xs_list_get(attach_file, 1)); - int fs = xs_number_get(xs_list_get(attach_file, 2)); + attach_list = xs_list_append(attach_list, l); + } + else + if (xs_is_list(attach_file)) { + const char *fn = xs_list_get(attach_file, 0); - /* store */ - static_put(&snac, id, payload + fo, fs); + if (xs_is_string(fn) && *fn != '\0') { + char *ext = strrchr(fn, '.'); + xs *hash = xs_md5_hex(fn, strlen(fn)); + xs *id = xs_fmt("%s%s", hash, ext); + xs *url = xs_fmt("%s/s/%s", snac.actor, id); + int fo = xs_number_get(xs_list_get(attach_file, 1)); + int fs = xs_number_get(xs_list_get(attach_file, 2)); - xs *l = xs_list_new(); + /* store */ + static_put(&snac, id, payload + fo, fs); - l = xs_list_append(l, url); - l = xs_list_append(l, alt_text); + xs *l = xs_list_new(); - attach_list = xs_list_append(attach_list, l); + l = xs_list_append(l, url); + l = xs_list_append(l, alt_text); + + attach_list = xs_list_append(attach_list, l); + } } } diff --git a/httpd.c b/httpd.c @@ -219,7 +219,8 @@ int server_get_handler(xs_dict *req, const char *q_path, if (xs_type(q_vars) == XSTYPE_DICT && (t = xs_dict_get(q_vars, "t"))) { /** search by tag **/ int skip = 0; - int show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries")); + int show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries", + xs_dict_get_def(srv_config, "max_timeline_entries", "50"))); const char *v; if ((v = xs_dict_get(q_vars, "skip")) != NULL) diff --git a/main.c b/main.c @@ -11,6 +11,7 @@ #include "snac.h" #include <sys/stat.h> +#include <sys/wait.h> int usage(void) { @@ -49,6 +50,7 @@ int usage(void) printf("unblock {basedir} {instance_url} Unblocks a full instance\n"); printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n"); printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n"); + printf("unmute {basedir} {uid} {actor} Unmutes a previously muted actor\n"); printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n"); printf("search {basedir} {uid} {regex} Searches posts by content\n"); printf("export_csv {basedir} {uid} Exports data as CSV files\n"); @@ -446,6 +448,18 @@ int main(int argc, char *argv[]) return 0; } + if (strcmp(cmd, "unmute") == 0) { /** **/ + if (is_muted(&snac, url)) { + unmute(&snac, url); + + printf("%s unmuted\n", url); + } + else + printf("%s actor is not muted\n", url); + + return 0; + } + if (strcmp(cmd, "search") == 0) { /** **/ int to; @@ -663,19 +677,25 @@ int main(int argc, char *argv[]) if (strcmp(url, "-e") == 0) { /* get the content from an editor */ +#define EDITOR "$EDITOR " + char cmd[] = EDITOR "/tmp/snac-XXXXXX"; FILE *f; - - unlink("/tmp/snac-edit.txt"); - system("$EDITOR /tmp/snac-edit.txt"); - - if ((f = fopen("/tmp/snac-edit.txt", "r")) != NULL) { - content = xs_readall(f); - fclose(f); - - unlink("/tmp/snac-edit.txt"); - } - else { - printf("Nothing to send\n"); + int fd = mkstemp(cmd + strlen(EDITOR)); + + if (fd >= 0) { + int status = system(cmd); + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0 && (f = fdopen(fd, "r")) != NULL) { + content = xs_readall(f); + fclose(f); + unlink(cmd + strlen(EDITOR)); + } else { + printf("Nothing to send\n"); + close(fd); + return 1; + } + } else { + fprintf(stderr, "Temp file creation failed\n"); return 1; } } @@ -687,6 +707,11 @@ int main(int argc, char *argv[]) else content = xs_dup(url); + if (!content || !*content) { + printf("Nothing to send\n"); + return 1; + } + int scope = 0; if (strcmp(cmd, "note_mention") == 0) scope = 1; diff --git a/mastoapi.c b/mastoapi.c @@ -1676,7 +1676,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, else if (strcmp(opt, "statuses") == 0) { /** **/ /* the public list of posts of a user */ - xs *timeline = timeline_simple_list(&snac2, "public", 0, 256); + xs *timeline = timeline_simple_list(&snac2, "public", 0, 256, NULL); xs_list *p = timeline; const xs_str *v; @@ -2171,7 +2171,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, { xs *d11 = xs_json_loads("{\"characters_reserved_per_url\":32," - "\"max_characters\":100000,\"max_media_attachments\":8}"); + "\"max_characters\":100000,\"max_media_attachments\":4}"); + + const xs_number *max_attachments = xs_dict_get(srv_config, "max_attachments"); + if (xs_type(max_attachments) == XSTYPE_NUMBER) + d11 = xs_dict_set(d11, "max_media_attachments", max_attachments); + cfg = xs_dict_append(cfg, "statuses", d11); xs *d12 = xs_json_loads("{\"max_featured_tags\":0}"); diff --git a/snac.c b/snac.c @@ -69,7 +69,7 @@ xs_str *tid(int offset) gettimeofday(&tv, NULL); - return xs_fmt("%10d.%06d", tv.tv_sec + offset, tv.tv_usec); + return xs_fmt("%010ld.%06ld", (long)tv.tv_sec + (long)offset, (long)tv.tv_usec); } diff --git a/snac.h b/snac.h @@ -1,7 +1,7 @@ /* snac - A simple, minimalistic ActivityPub instance */ /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ -#define VERSION "2.69" +#define VERSION "2.72" #define USER_AGENT "snac/" VERSION @@ -22,6 +22,8 @@ #define MD5_HEX_SIZE 33 +#define MD5_ALREADY_SEEN_MARK "00000000000000000000000000000000" + extern double disk_layout; extern xs_str *srv_basedir; extern xs_dict *srv_config; @@ -157,12 +159,14 @@ int timeline_here(snac *snac, const char *md5); int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); int timeline_del(snac *snac, const char *id); xs_str *user_index_fn(snac *user, const char *idx_name); -xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show); -xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show); +xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more); +xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more); int timeline_add(snac *snac, const char *id, const xs_dict *o_msg); int timeline_admire(snac *snac, const char *id, const char *admirer, int like); xs_list *timeline_top_level(snac *snac, const xs_list *list); +void timeline_add_mark(snac *user); + xs_list *local_list(snac *snac, int max); xs_str *instance_index_fn(void); xs_list *timeline_instance_list(int skip, int show); diff --git a/utils.c b/utils.c @@ -28,6 +28,7 @@ static const char *default_srv_config = "{" "\"queue_timeout\": 6," "\"queue_timeout_2\": 8," "\"cssurls\": [\"\"]," + "\"def_timeline_entries\": 50," "\"max_timeline_entries\": 50," "\"timeline_purge_days\": 120," "\"local_purge_days\": 0," @@ -36,6 +37,7 @@ static const char *default_srv_config = "{" "\"admin_account\": \"\"," "\"title\": \"\"," "\"short_description\": \"\"," + "\"short_description_raw\": false," "\"protocol\": \"https\"," "\"fastcgi\": false" "}"; @@ -72,6 +74,7 @@ static const char *default_css = ".snac-list-of-lists { padding-left: 0; }\n" ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n" " margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n" + ".snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; }\n" "@media (prefers-color-scheme: dark) { \n" " body, input, textarea { background-color: #000; color: #fff; }\n" " a { color: #7799dd }\n" diff --git a/xs.h b/xs.h @@ -12,6 +12,7 @@ #include <stdarg.h> #include <signal.h> #include <errno.h> +#include <stdint.h> typedef enum { XSTYPE_STRING = 0x02, /* C string (\0 delimited) (NOT STORED) */ @@ -142,6 +143,7 @@ void xs_data_get(void *data, const xs_data *value); void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size); unsigned int xs_hash_func(const char *data, int size); +uint64_t xs_hash64_func(const char *data, int size); #ifdef XS_ASSERT #include <assert.h> @@ -632,7 +634,7 @@ xs_str *xs_crop_i(xs_str *str, int start, int end) end = sz + end; /* crop from the top */ - if (end > 0 && end < sz) + if (end >= 0 && end < sz) str[end] = '\0'; /* crop from the bottom */ @@ -989,16 +991,20 @@ xs_str *xs_join(const xs_list *list, const char *sep) xs_list *xs_split_n(const char *str, const char *sep, int times) /* splits a string into a list upto n times */ { + xs_list *list = xs_list_new(); + + if (!xs_is_string(str) || !xs_is_string(sep)) + return list; + int sz = strlen(sep); char *ss; - xs_list *list; - - list = xs_list_new(); while (times > 0 && (ss = strstr(str, sep)) != NULL) { /* create a new string with this slice and add it to the list */ xs *s = xs_str_new_sz(str, ss - str); - list = xs_list_append(list, s); + + if (xs_is_string(s)) + list = xs_list_append(list, s); /* skip past the separator */ str = ss + sz; @@ -1007,7 +1013,8 @@ xs_list *xs_split_n(const char *str, const char *sep, int times) } /* add the rest of the string */ - list = xs_list_append(list, str); + if (xs_is_string(str)) + list = xs_list_append(list, str); return list; } @@ -1487,9 +1494,8 @@ unsigned int xs_hash_func(const char *data, int size) /* a general purpose hashing function */ { unsigned int hash = 0x666; - int n; - for (n = 0; n < size; n++) { + for (int n = 0; n < size; n++) { hash ^= (unsigned char)data[n]; hash *= 111111111; } @@ -1498,6 +1504,20 @@ unsigned int xs_hash_func(const char *data, int size) } +uint64_t xs_hash64_func(const char *data, int size) +/* a general purpose hashing function (64 bit) */ +{ + uint64_t hash = 0x100; + + for (int n = 0; n < size; n++) { + hash ^= (unsigned char)data[n]; + hash *= 1111111111111111111; + } + + return hash; +} + + #endif /* XS_IMPLEMENTATION */ #endif /* _XS_H */ diff --git a/xs_fcgi.h b/xs_fcgi.h @@ -173,6 +173,9 @@ xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *fcgi_id) xs *v = xs_str_new_sz((char *)&buf[offset], vsz); offset += vsz; + if (!xs_is_string(k) || !xs_is_string(v)) + continue; + cgi_vars = xs_dict_append(cgi_vars, k, v); if (strcmp(k, "REQUEST_METHOD") == 0) diff --git a/xs_html.h b/xs_html.h @@ -114,7 +114,7 @@ xs_html *xs_html_text(const char *content) xs_html *a = XS_HTML_NEW(); a->type = XS_HTML_TEXT; - a->content = xs_html_encode(content); + a->content = xs_is_string(content) ? xs_html_encode(content) : xs_str_new(NULL); return a; } @@ -126,7 +126,7 @@ xs_html *xs_html_raw(const char *content) xs_html *a = XS_HTML_NEW(); a->type = XS_HTML_TEXT; - a->content = xs_dup(content); + a->content = xs_is_string(content) ? xs_dup(content) : xs_str_new(NULL); return a; } diff --git a/xs_httpd.h b/xs_httpd.h @@ -15,41 +15,48 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size) { xs *q_vars = NULL; xs *p_vars = NULL; - xs *l1, *l2; + xs *l1; const char *v; + char *saveptr; xs_socket_timeout(fileno(f), 2.0, 0.0); /* read the first line and split it */ l1 = xs_strip_i(xs_readline(f)); - l2 = xs_split(l1, " "); + char *raw_path; + const char *mtd; + const char *proto; + + if (!(mtd = strtok_r(l1, " ", &saveptr)) || + !(raw_path = strtok_r(NULL, " ", &saveptr)) || + !(proto = strtok_r(NULL, " ", &saveptr)) || + strtok_r(NULL, " ", &saveptr)) + return NULL; - if (xs_list_len(l2) != 3) { - /* error or timeout */ + if (!xs_is_string(mtd) || !xs_is_string(raw_path) || !xs_is_string(proto)) return NULL; - } xs_dict *req = xs_dict_new(); - req = xs_dict_append(req, "method", xs_list_get(l2, 0)); - req = xs_dict_append(req, "raw_path", xs_list_get(l2, 1)); - req = xs_dict_append(req, "proto", xs_list_get(l2, 2)); + req = xs_dict_append(req, "method", mtd); + req = xs_dict_append(req, "raw_path", raw_path); + req = xs_dict_append(req, "proto", proto); { - /* split the path with its optional variables */ - const xs_val *udp = xs_list_get(l2, 1); - xs *pnv = xs_split_n(udp, "?", 1); - - /* store the path */ - req = xs_dict_append(req, "path", xs_list_get(pnv, 0)); + char *q = strchr(raw_path, '?'); /* get the variables */ - q_vars = xs_url_vars(xs_list_get(pnv, 1)); + if (q) { + *q++ = '\0'; + q_vars = xs_url_vars(q); + } + /* store the path */ + req = xs_dict_append(req, "path", raw_path); } /* read the headers */ for (;;) { - xs *l, *p = NULL; + xs *l; l = xs_strip_i(xs_readline(f)); @@ -58,11 +65,18 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size) break; /* split header and content */ - p = xs_split_n(l, ": ", 1); + char *cnt = strchr(l, ':'); + if (!cnt) + continue; + + *cnt++ = '\0'; + cnt += strspn(cnt, " \r\n\t\v\f"); + l = xs_rstrip_chars_i(l, " \r\n\t\v\f"); + + if (!xs_is_string(cnt)) + continue; - if (xs_list_len(p) == 2) - req = xs_dict_append(req, xs_tolower_i( - (xs_str *)xs_list_get(p, 0)), xs_list_get(p, 1)); + req = xs_dict_append(req, xs_tolower_i(l), cnt); } xs_socket_timeout(fileno(f), 5.0, 0.0); diff --git a/xs_io.h b/xs_io.h @@ -14,7 +14,7 @@ xs_val *xs_readall(FILE *f); xs_str *xs_readline(FILE *f) /* reads a line from a file */ { - xs_str *s = NULL; + xs_str *s = xs_str_new(NULL); errno = 0; @@ -22,12 +22,11 @@ xs_str *xs_readline(FILE *f) if (!feof(f)) { int c; - s = xs_str_new(NULL); - while ((c = fgetc(f)) != EOF) { unsigned char rc = c; - s = xs_append_m(s, (char *)&rc, 1); + if (xs_is_string((char *)&rc)) + s = xs_append_m(s, (char *)&rc, 1); if (c == '\n') break; diff --git a/xs_json.h b/xs_json.h @@ -280,6 +280,12 @@ static xs_val *_xs_json_load_lexer(FILE *f, js_type *t) else { char cc = c; v = xs_insert_m(v, offset, &cc, 1); + + if (!xs_is_string(v)) { + *t = JS_ERROR; + break; + } + offset++; } } diff --git a/xs_match.h b/xs_match.h @@ -24,6 +24,7 @@ int xs_match(const char *str, const char *spec) retry: for (;;) { + const char *q = spec; char c = *str++; char p = *spec++; @@ -63,8 +64,12 @@ retry: spec = b_spec; str = ++b_str; } - else + else { + if (*q == '|') + spec = q; + break; + } } } } diff --git a/xs_openssl.h b/xs_openssl.h @@ -83,7 +83,7 @@ xs_val *xs_base64_dec(const xs_str *data, int *size) s = xs_realloc(s, _xs_blk_size(*size + 1)); s[*size] = '\0'; - BIO_free_all(mem); + BIO_free_all(b64); return s; } diff --git a/xs_socket.h b/xs_socket.h @@ -85,6 +85,8 @@ int xs_socket_server(const char *addr, const char *serv) listen(rs, SOMAXCONN); } + freeaddrinfo(res); + #else /* WITHOUT_GETADDRINFO */ struct sockaddr_in host; diff --git a/xs_url.h b/xs_url.h @@ -11,18 +11,57 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea #ifdef XS_IMPLEMENTATION +char *xs_url_dec_in(char *str, int qs) +{ + char *w = str; + char *r; + + for (r = str; *r != '\0'; r++) { + switch (*r) { + case '%': { + unsigned hex; + if (!r[1] || !r[2]) + return NULL; + if (sscanf(r + 1, "%2x", &hex) != 1) + return NULL; + *w++ = hex; + r += 2; + break; + } + + case '+': + if (qs) { + *w++ = ' '; + break; + } + /* fall-through */ + default: + *w++ = *r; + } + } + + *w++ = '\0'; + return str; +} + xs_str *xs_url_dec(const char *str) /* decodes an URL */ { xs_str *s = xs_str_new(NULL); while (*str) { + if (!xs_is_string(str)) + break; + if (*str == '%') { unsigned int i; if (sscanf(str + 1, "%02x", &i) == 1) { unsigned char uc = i; + if (!xs_is_string((char *)&uc)) + break; + s = xs_append_m(s, (char *)&uc, 1); str += 2; } @@ -69,43 +108,45 @@ xs_dict *xs_url_vars(const char *str) vars = xs_dict_new(); - if (str != NULL) { - /* split by arguments */ - xs *args = xs_split(str, "&"); - - const xs_val *v; - - xs_list_foreach(args, v) { - xs *dv = xs_url_dec(v); - xs *kv = xs_split_n(dv, "=", 1); - - if (xs_list_len(kv) == 2) { - const char *key = xs_list_get(kv, 0); - const char *pv = xs_dict_get(vars, key); - - if (!xs_is_null(pv)) { - /* there is a previous value: convert to a list and append */ - xs *vlist = NULL; - if (xs_type(pv) == XSTYPE_LIST) - vlist = xs_dup(pv); - else { - vlist = xs_list_new(); - vlist = xs_list_append(vlist, pv); - } - - vlist = xs_list_append(vlist, xs_list_get(kv, 1)); - vars = xs_dict_set(vars, key, vlist); - } + if (xs_is_string(str)) { + xs *dup = xs_dup(str); + char *k; + char *saveptr; + for (k = strtok_r(dup, "&", &saveptr); + k; + k = strtok_r(NULL, "&", &saveptr)) { + char *v = strchr(k, '='); + if (!v) + continue; + *v++ = '\0'; + k = xs_url_dec_in(k, 1); + v = xs_url_dec_in(v, 1); + if (!xs_is_string(k) || !xs_is_string(v)) + continue; + + const char *pv = xs_dict_get(vars, k); + if (!xs_is_null(pv)) { + /* there is a previous value: convert to a list and append */ + xs *vlist = NULL; + if (xs_type(pv) == XSTYPE_LIST) + vlist = xs_dup(pv); else { - /* ends with []? force to always be a list */ - if (xs_endswith(key, "[]")) { - xs *vlist = xs_list_new(); - vlist = xs_list_append(vlist, xs_list_get(kv, 1)); - vars = xs_dict_append(vars, key, vlist); - } - else - vars = xs_dict_append(vars, key, xs_list_get(kv, 1)); + vlist = xs_list_new(); + vlist = xs_list_append(vlist, pv); + } + + vlist = xs_list_append(vlist, v); + vars = xs_dict_set(vars, k, vlist); + } + else { + /* ends with []? force to always be a list */ + if (xs_endswith(k, "[]")) { + xs *vlist = xs_list_new(); + vlist = xs_list_append(vlist, v); + vars = xs_dict_append(vars, k, vlist); } + else + vars = xs_dict_append(vars, k, v); } } } @@ -233,7 +274,8 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea l1 = xs_list_append(l1, vpo); l1 = xs_list_append(l1, vps); - p_vars = xs_dict_append(p_vars, vn, l1); + if (xs_is_string(vn)) + p_vars = xs_dict_append(p_vars, vn, l1); } else { /* regular variable; just copy */ @@ -241,7 +283,8 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea memcpy(vc, payload + po, ps); vc[ps] = '\0'; - p_vars = xs_dict_append(p_vars, vn, vc); + if (xs_is_string(vn) && xs_is_string(vc)) + p_vars = xs_dict_append(p_vars, vn, vc); } /* move on */ diff --git a/xs_version.h b/xs_version.h @@ -1 +1 @@ -/* b865e89769aedfdbc61251e94451e9d37579f52e 2025-01-12T16:17:47+01:00 */ +/* 2f43b93e9d2b63360c802e09f4c68adfef74c673 2025-01-28T07:40:50+01:00 */