From 799d6de1416b964ee846a2d59d60fc3e3306ba1e Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 15 Apr 2026 09:14:56 +0200 Subject: Another fix to %s null. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index a3d2c61..c17583e 100644 --- a/html.c +++ b/html.c @@ -5358,7 +5358,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, uid = xs_list_get(l, 1); if (!uid || !user_open(&snac, uid)) { /* invalid user */ - srv_debug(1, xs_fmt("html_post_handler bad user %s", uid)); + srv_debug(1, xs_fmt("html_post_handler bad user %s", xs_or(uid, "(null)"))); return HTTP_STATUS_NOT_FOUND; } -- cgit From 4db4958ba965605e088eb177fcc0c8b7d9cafadf Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 23 Apr 2026 09:14:23 +0200 Subject: Don't recalculate md5 in » links. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- html.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index c17583e..c7bbb87 100644 --- a/html.c +++ b/html.c @@ -321,13 +321,11 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, name_link); if (!xs_is_null(url)) { - xs *md5 = xs_md5_hex(url, strlen(url)); - xs_html_add(actor_icon, xs_html_text(" "), xs_html_tag("a", xs_html_attr("href", (char *)url), - xs_html_attr("title", md5), + xs_html_attr("title", xs_is_string(md5) ? md5 : ""), xs_html_text("»"))); } -- cgit From c307e99141e27b8d2bab035a08a9e7ce3af7d03f Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 27 Apr 2026 08:07:32 +0200 Subject: In URL searches, try first the ActivityPub object, then the Webfinger. This fixes an error when searching for GotoSocial statuses, as they are incorrectly identified as accounts. --- html.c | 62 ++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 32 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index c7bbb87..3fd819f 100644 --- a/html.c +++ b/html.c @@ -4761,48 +4761,46 @@ int html_get_handler(const xs_dict *req, const char *q_path, /* searching for an URL? */ if (q && xs_match(q, "https://*|http://*")) { - /* may by an actor; try a webfinger */ + /* bring it to the user's timeline */ xs *actor_obj = NULL; + xs *object = NULL; + int status; - if (valid_status(webfinger_request(q, &actor_obj, &url_acct)) && xs_is_string(url_acct)) { - /* it's an actor; do the dirty trick of changing q to the account name */ - q = url_acct; - } - else { - /* bring it to the user's timeline */ - xs *object = NULL; - int status; - - status = activitypub_request(&snac, q, &object); - snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status)); + status = activitypub_request(&snac, q, &object); + snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status)); - if (valid_status(status)) { - /* got it; also request the actor */ - const char *attr_to = get_atto(object); + if (valid_status(status) && xs_is_dict(object)) { + /* got it; also request the actor */ + const char *attr_to = get_atto(object); - if (!xs_is_null(attr_to)) { - status = actor_request(&snac, attr_to, &actor_obj); + if (!xs_is_null(attr_to)) { + status = actor_request(&snac, attr_to, &actor_obj); - if (valid_status(status)) { - /* reset the query string to be the real id */ - url_acct = xs_dup(xs_dict_get(object, "id")); - q = url_acct; + if (valid_status(status)) { + /* reset the query string to be the real id */ + url_acct = xs_dup(xs_dict_get(object, "id")); + q = url_acct; - /* add the post to the timeline */ - if (!timeline_here(&snac, q)) - timeline_add(&snac, q, object); - } + /* add the post to the timeline */ + if (!timeline_here(&snac, q)) + timeline_add(&snac, q, object); } - else { - /* retry webfinger, this time with the 'official' id */ - const char *id = xs_dict_get(object, "id"); + } + else { + /* retry webfinger, this time with the 'official' id */ + const char *id = xs_dict_get(object, "id"); - if (xs_is_string(id) && valid_status(webfinger_request(id, &actor_obj, &url_acct)) && - xs_is_string(url_acct)) - q = url_acct; - } + if (xs_is_string(id) && valid_status(webfinger_request(id, &actor_obj, &url_acct)) && + xs_is_string(url_acct)) + q = url_acct; } } + else + /* may by an actor; try a webfinger */ + if (valid_status(webfinger_request(q, &actor_obj, &url_acct)) && xs_is_string(url_acct)) { + /* it's an actor; do the dirty trick of changing q to the account name */ + q = url_acct; + } /* fall through */ } -- cgit From 6f11524c1cd9eb82bbbdaa06473030392a9a06fb Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 27 Apr 2026 18:28:05 +0200 Subject: More webfinger search tweaks. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 3fd819f..3f57be6 100644 --- a/html.c +++ b/html.c @@ -4806,7 +4806,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, } if (q && *q) { - if (xs_regex_match(q, "^@?[a-zA-Z0-9._]+@[a-zA-Z0-9-]+\\.")) { + if (xs_regex_match(q, "^@?[^@]+@[a-zA-Z0-9-]+\\.")) { /** search account **/ xs *actor = NULL; xs *acct = NULL; -- cgit From 61971f2792185e32dfa31de22d2f168b60f6c610 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 7 May 2026 10:28:59 +0200 Subject: Show a thumb up next to account names that follow you. --- html.c | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index 3f57be6..dbc65c8 100644 --- a/html.c +++ b/html.c @@ -345,6 +345,14 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, xs_html_attr("title", "mutual relation"), xs_html_raw("🤝"))); } + else + if (fwer) { + xs_html_add(actor_icon, + xs_html_text(" "), + xs_html_tag("span", + xs_html_attr("title", "follows you"), + xs_html_raw("👍"))); + } if (instance_failure(actor_id, 0) == -1) { xs_html_add(actor_icon, -- cgit From d9d4465eb4bae643fc037cba7738a66cd1cb508d Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 18 May 2026 17:08:11 +0200 Subject: Limited the number of nested quoted posts to 3. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index dbc65c8..cd1746a 100644 --- a/html.c +++ b/html.c @@ -2816,7 +2816,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, /* quoted post */ const char *quoted_id = xs_or(xs_dict_get(msg, "quoteUri"), xs_dict_get(msg, "quoteUrl")); - if (xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/ + if (level < 3 && xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/ xs *quoted_post = NULL; if (valid_status(object_get(quoted_id, "ed_post))) { -- cgit From d3309f8b5e43c8116f99e548bb9f874b5cdaf70e Mon Sep 17 00:00:00 2001 From: violette Date: Thu, 21 May 2026 19:40:58 +0200 Subject: Added a new 'admiration' timeline. --- data.c | 5 ++++- html.c | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/data.c b/data.c index 7fb261c..ad71b85 100644 --- a/data.c +++ b/data.c @@ -1606,12 +1606,15 @@ int timeline_admire(snac *snac, const char *id, const char *content = xs_dict_get_path(msg, "content"); const char *type = xs_dict_get_path(msg, "type"); - /* if we are admiring this, add to both timelines */ + /* if we are admiring this, add to both timelines, and store for later */ if (!like && strcmp(admirer, snac->actor) == 0) { object_user_cache_add(snac, id, "public"); object_user_cache_add(snac, id, "private"); } + if (strcmp(admirer, snac->actor) == 0) + object_user_cache_add(snac, id, "admire"); + /* use utf <3 as a like, as it is ugly */ if (type && xs_match(type, "Like|EmojiReact|Emoji") && content && strcmp(content, "❤") != 0) { diff --git a/html.c b/html.c index cd1746a..29dd8e8 100644 --- a/html.c +++ b/html.c @@ -3520,6 +3520,18 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, xs_html_text(L("pinned"))))); } + { + /* show the list of bookmarked posts */ + xs *url = xs_fmt("%s/admirations", user->actor); + xs_html_add(lol, + xs_html_tag("li", + xs_html_tag("a", + xs_html_attr("href", url), + xs_html_attr("class", "snac-list-link"), + xs_html_attr("title", L("Liked, boosted or reacted posts")), + xs_html_text(L("admirations"))))); + } + { /* show the list of bookmarked posts */ xs *url = xs_fmt("%s/bookmarks", user->actor); @@ -5042,6 +5054,22 @@ int html_get_handler(const xs_dict *req, const char *q_path, } } else + if (strcmp(p_path, "admirations") == 0) { /** list of admired posts **/ + if (!login(&snac, req)) { + *body = xs_dup(uid); + status = HTTP_STATUS_UNAUTHORIZED; + } + else { + int more = 0; + xs *list = timeline_list(&snac, "admire", skip, show, &more); + + *body = html_timeline(&snac, list, 0, skip, show, + more, L("Liked, boosted or reacted posts"), "/admirations", 0, error, terse); + *b_size = strlen(*body); + status = HTTP_STATUS_OK; + } + } + else if (strcmp(p_path, "bookmarks") == 0) { /** list of bookmarked posts **/ if (!login(&snac, req)) { *body = xs_dup(uid); -- cgit From a4381e41db43e2577aa5525586ab4ee680f3fec0 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 22 May 2026 08:01:16 +0200 Subject: Delete from the admire index when unliking and unboosting. --- html.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index 29dd8e8..f922ae2 100644 --- a/html.c +++ b/html.c @@ -5703,6 +5703,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, xs *msg = msg_repulsion(&snac, id, "Like"); if (msg != NULL) { + object_user_cache_del(&snac, id, "admire"); enqueue_message(&snac, msg); } } @@ -5711,6 +5712,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, xs *msg = msg_repulsion(&snac, id, "Announce"); if (msg != NULL) { + object_user_cache_del(&snac, id, "admire"); enqueue_message(&snac, msg); } } -- cgit From e5b62f675dd49f1e7a9bc797f48aaf503c6537e8 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 22 May 2026 08:56:44 +0200 Subject: Delete from the admire cache also when emojiunreacting. --- html.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index f922ae2..ecd7fd7 100644 --- a/html.c +++ b/html.c @@ -5661,6 +5661,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, const char *eid = xs_dict_get(p_vars, "eid"); if (eid != NULL) { + object_user_cache_del(&snac, id, "admire"); xs *n_msg = msg_emoji_unreact(&snac, id, eid); if (n_msg != NULL) @@ -5675,8 +5676,10 @@ int html_post_handler(const xs_dict *req, const char *q_path, xs *ret = msg_emoji_init(&snac, id, eid); /* fails if either invalid or already reacted */ - if (!ret) + if (!ret) { + object_user_cache_del(&snac, id, "admire"); ret = msg_emoji_unreact(&snac, id, eid); + } if (!ret) status = HTTP_STATUS_NOT_FOUND; } -- cgit From 5182a3384e80462e08713903c14b9cdecf56cdfb Mon Sep 17 00:00:00 2001 From: hanchan Date: Wed, 27 May 2026 17:26:33 +0800 Subject: add keep_replied_posts and keep_replied_me feathers, and auto-mention post author when reply to a post --- data.c | 31 +++++++++++++++++++++++++++++++ doc/snac.8 | 5 +++++ html.c | 21 +++++++++++++++++++++ utils.c | 4 +++- 4 files changed, 60 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/data.c b/data.c index 37c9325..3f7ba29 100644 --- a/data.c +++ b/data.c @@ -1562,6 +1562,37 @@ void timeline_update_indexes(snac *snac, const char *id) else /* also add it to public, it will be discarded later */ object_user_cache_add(snac, id, "public"); + + /* if `keep_replied_posts` is `true` and a local post is a reply to a remote post, add the parent remote post to the public */ + if (xs_is_true(xs_dict_get(srv_config, "keep_replied_posts"))) { + const char *in_reply_to = get_in_reply_to(msg); + + if (xs_is_string(in_reply_to) && *in_reply_to && + !is_msg_mine(snac, in_reply_to)) { + if (object_user_cache_add(snac, in_reply_to, "public") >= 0) { + snac_debug(snac, 1, + xs_fmt("keep_replied_posts: added parent %s to public", in_reply_to)); + } + } + } + } + } + else { + /* if `keep_replied_me` is `true` and a remote post is a reply to one of local posts, add it to public */ + if (xs_is_true(xs_dict_get(srv_config, "keep_replied_me"))) { + xs *msg = NULL; + + if (valid_status(object_get(id, &msg))) { + const char *in_reply_to = get_in_reply_to(msg); + + if (xs_is_string(in_reply_to) && *in_reply_to && + is_msg_mine(snac, in_reply_to)) { + if (object_user_cache_add(snac, id, "public") >= 0) { + snac_debug(snac, 1, + xs_fmt("keep_replied_me: added reply %s to public", id)); + } + } + } } } } diff --git a/doc/snac.8 b/doc/snac.8 index ef93d31..5698ce1 100644 --- a/doc/snac.8 +++ b/doc/snac.8 @@ -327,6 +327,11 @@ Overrides the default "mogrify" command name or path. Use this if the tool is no Overrides the default "ffmpeg" command name or path. Use this if the tool is not in the system PATH or has a different name. .It Ic purge_static If set to true, user static directories are purged; any file stored in these subdirectories (usually images or other media) that is not attached to any post, not used as avatar, header or favicon, and not served as an emoji, is deleted from the filesystem. Don't enable this option if any of your users are serving other files (like, e.g., CSS assets) this way. +.It Ic keep_replied_posts +If set to true, when you reply to a remote post, the parent remote post will be added to local public timeline. +.It Ic keep_replied_me +If set to true, when a remote user replies to one of local posts, the +remote reply will be added to local public timeline. .El .Pp You must restart the server to make effective these changes. diff --git a/html.c b/html.c index ecd7fd7..a553d8c 100644 --- a/html.c +++ b/html.c @@ -2035,6 +2035,27 @@ xs_str *build_mentions(snac *user, const xs_dict *msg) } } + /* also add the author of this post as a mention */ + const char *atto = get_atto(msg); + + if (xs_is_string(atto) && *atto && strcmp(atto, user->actor) != 0) { + /* check if this author is already in the mentions string */ + xs *l = xs_split(atto, "/"); + + if (xs_list_len(l) > 3) { + xs *actor_o = NULL; + if (valid_status(object_get(atto, &actor_o))) { + const char *uname = xs_dict_get(actor_o, "preferredUsername"); + if (!xs_is_null(uname) && *uname) { + xs *handle = xs_fmt("@%s@%s ", uname, xs_list_get(l, 2)); + if (xs_str_in(s, handle) == -1) { + s = xs_str_cat(s, handle); + } + } + } + } + } + if (*s) { xs *s1 = s; s = xs_fmt("\n\n\nCC: %s", s1); diff --git a/utils.c b/utils.c index c36587a..9475bd7 100644 --- a/utils.c +++ b/utils.c @@ -44,7 +44,9 @@ static const char * const default_srv_config = "{" "\"protocol\": \"https\"," "\"fastcgi\": false," "\"disable_inbox_collection\": true," - "\"disable_history\": true" + "\"disable_history\": true," + "\"keep_replied_posts\": false," + "\"keep_replied_me\": false" "}"; static const char * const default_css = -- cgit From 1adf45de30921ae727af2dd1302615e33e8181ba Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 2 Jun 2026 08:56:16 +0200 Subject: Minor cleanup. --- html.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index a553d8c..76d211c 100644 --- a/html.c +++ b/html.c @@ -3447,7 +3447,6 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, int utl, const char *error, int terse) /* returns the HTML for the timeline */ { - xs_list *p = (xs_list *)list; const char *v; double t = ftime(); int hide_children = xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")) && read_only; @@ -3639,7 +3638,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, int show_unlisted = user ? xs_is_true(xs_dict_get(user->config, "show_unlisted")) : 0; - while (xs_list_iter(&p, &v)) { + xs_list_foreach(list, v) { xs *msg = NULL; int status; -- cgit From 41fea226322ca945ffec574a2f69d0b6adb63ff2 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 2 Jun 2026 09:19:38 +0200 Subject: New user directive: 'excluded_langs'. --- html.c | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index 76d211c..6ddd252 100644 --- a/html.c +++ b/html.c @@ -2339,6 +2339,17 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, int c = 0; xs_dict_next(cmap, &lang, &dummy, &c); + + if (user && xs_is_string(lang)) { + /* discard posts in excluded languages */ + const char *excluded_langs = xs_dict_get(user->config, "excluded_langs"); + + if (xs_is_string(excluded_langs) && + xs_str_in(excluded_langs, lang) != -1) { + snac_debug(user, 1, xs_fmt("excluded post in language '%s'", lang)); + return NULL; + } + } } if (strcmp(type, "Follow") == 0) { -- cgit From e5696072023056552a997a499a3a8399b20ed431 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 2 Jun 2026 09:38:09 +0200 Subject: Added web UI to set the 'excluded_langs'. --- html.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index 6ddd252..aaf0346 100644 --- a/html.c +++ b/html.c @@ -1613,6 +1613,7 @@ xs_html *html_top_controls(snac *user) const char *longitude = xs_dict_get_def(user->config, "longitude", ""); const char *webhook = xs_dict_get_def(user->config, "notify_webhook", ""); const char *post_langs = xs_dict_get_def(user->config, "post_langs", ""); + const char *excluded_langs = xs_dict_get_def(user->config, "excluded_langs", ""); xs *metadata = NULL; const xs_dict *md = xs_dict_get(user->config, "metadata"); @@ -1850,6 +1851,15 @@ xs_html *html_top_controls(snac *user) xs_html_attr("value", post_langs), xs_html_attr("placeholder", L("en fr es de_AT")))), + xs_html_tag("p", + xs_html_text(L("Exclude posts written in these languages:")), + xs_html_sctag("br", NULL), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", "excluded_langs"), + xs_html_attr("value", excluded_langs), + xs_html_attr("placeholder", L("en fr es de_AT")))), + xs_html_tag("p", xs_html_text(L("New password:")), xs_html_sctag("br", NULL), @@ -5991,6 +6001,10 @@ int html_post_handler(const xs_dict *req, const char *q_path, snac.config = xs_dict_set(snac.config, "tz", v); if ((v = xs_dict_get(p_vars, "post_langs")) != NULL) snac.config = xs_dict_set(snac.config, "post_langs", v); + if ((v = xs_dict_get(p_vars, "excluded_langs")) != NULL) + snac.config = xs_dict_set(snac.config, "excluded_langs", v); + else + snac.config = xs_dict_del(snac.config, "excluded_langs"); snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", "")); snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", "")); -- cgit From c279fc8f6ac14283400e8175a2faed6270739413 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 2 Jun 2026 09:43:19 +0200 Subject: Minor rewording. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index aaf0346..80b7546 100644 --- a/html.c +++ b/html.c @@ -1852,7 +1852,7 @@ xs_html *html_top_controls(snac *user) xs_html_attr("placeholder", L("en fr es de_AT")))), xs_html_tag("p", - xs_html_text(L("Exclude posts written in these languages:")), + xs_html_text(L("Don't show posts written in these languages:")), xs_html_sctag("br", NULL), xs_html_sctag("input", xs_html_attr("type", "text"), -- cgit From 4414ed4d7d2168bdb1132f54520318f2e55db4d0 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 8 Jun 2026 18:36:32 +0200 Subject: Another tweak to search. Call webfinger_request_fake() for extreme cases. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 80b7546..5ba00ba 100644 --- a/html.c +++ b/html.c @@ -4851,7 +4851,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, /* retry webfinger, this time with the 'official' id */ const char *id = xs_dict_get(object, "id"); - if (xs_is_string(id) && valid_status(webfinger_request(id, &actor_obj, &url_acct)) && + if (xs_is_string(id) && valid_status(webfinger_request_fake(id, &actor_obj, &url_acct)) && xs_is_string(url_acct)) q = url_acct; } -- cgit