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:
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 */